diff --git a/.github/workflows/desktop-regression.yml b/.github/workflows/desktop-regression.yml index 14febda..9d36356 100644 --- a/.github/workflows/desktop-regression.yml +++ b/.github/workflows/desktop-regression.yml @@ -1,4 +1,4 @@ -name: Run android regression tests +name: Run desktop regression tests on: workflow_dispatch: inputs: @@ -7,15 +7,160 @@ on: required: true type: choice options: - - oxen-io/session-playwright + - session-foundation/session-playwright - burtonemily/session-playwright - bilb/session-playwright - default: oxen-io/session-playwright + default: session-foundation/session-playwright + + BRANCH_TO_CHECKOUT_PW: + description: 'branch to checkout on session-playwright' + required: true + type: string + default: ci-desktop-regression-self-hosted + + SESSION_DESKTOP_REPO: + description: 'Session desktop repo to checkout' + required: true + type: choice + options: + - session-foundation/session-desktop + - bilb/session-desktop + default: session-foundation/session-desktop + + BRANCH_TO_CHECKOUT_SESSION: + description: 'Branch to checkout on session-desktop' + required: true + type: string + default: unstable + + PLAYWRIGHT_REPEAT_COUNT: + description: 'Repeats of each tests (0 to only run each once)' + required: true + type: number + default: 0 + + PLAYWRIGHT_RETRIES_COUNT: + description: 'Retries of each tests (0 to only run each once, 1 to run another attempt)' + required: true + type: number + default: 0 + + PLAYWRIGHT_WORKER_COUNT: + description: 'Playwright workers to start' + required: true + type: number + default: 6 + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: true jobs: desktop-regression: name: Desktop Regression Tests - runs-on: [self-hosted, linux, x64, qa-desktop] + runs-on: [self-hosted, Linux, X64, qa-desktop] + container: + image: mcr.microsoft.com/playwright:v1.47.1-noble + options: --cpus 16 + + env: + PLAYWRIGHT_CUSTOM_REPORTER: 1 + PLAYWRIGHT_REPEAT_COUNT: ${{ github.event.inputs.PLAYWRIGHT_REPEAT_COUNT }} + PLAYWRIGHT_RETRIES_COUNT: ${{ github.event.inputs.PLAYWRIGHT_RETRIES_COUNT }} + PLAYWRIGHT_WORKER_COUNT: ${{ github.event.inputs.PLAYWRIGHT_WORKER_COUNT }} + DESKTOP_CACHED_FOLDER: desktop/node_modules steps: - uses: actions/checkout@v4 + - name: Runner Details + run: | + echo "PLAYWRIGHT_REPO ${{ github.event.inputs.PLAYWRIGHT_REPO }}" + echo "BRANCH_TO_CHECKOUT_PW ${{ github.event.inputs.BRANCH_TO_CHECKOUT_PW }}" + echo "SESSION_DESKTOP_REPO ${{ github.event.inputs.SESSION_DESKTOP_REPO }}" + echo "BRANCH_TO_CHECKOUT_SESSION ${{ github.event.inputs.BRANCH_TO_CHECKOUT_SESSION }}" + + - uses: actions/checkout@v4 + name: 'Checkout playwright' + with: + repository: ${{ github.event.inputs.PLAYWRIGHT_REPO }} + ref: ${{ github.event.inputs.BRANCH_TO_CHECKOUT_PW }} + path: 'playwright' + + - name: Install system deps + run: apt update && apt install -y git g++ build-essential cmake + + - uses: actions/checkout@v4 + name: 'Checkout Session desktop' + with: + repository: ${{ github.event.inputs.SESSION_DESKTOP_REPO }} + ref: ${{ github.event.inputs.BRANCH_TO_CHECKOUT_SESSION }} + path: 'desktop' + + - name: Calculate desktop cache key + run: | + echo "CACHE_KEY=${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('desktop/package.json', 'desktop/yarn.lock', 'desktop/patches/**') }}" >> $GITHUB_ENV + + - name: Install node + uses: actions/setup-node@v3 + with: + node-version-file: 'desktop/.nvmrc' + + - uses: actions/setup-python@v4 + with: + python-version: '3.12' + + - uses: actions/cache/restore@v4 + id: cache-desktop-modules + with: + path: ${{ env.DESKTOP_CACHED_FOLDER }} + key: ${{ env.CACHE_KEY }} + + - name: Install yarn + run: | + npm install -g yarn + + - name: List desktop folder + run: | + pwd + ls -la desktop + + - name: List playwright folder + run: | + pwd + ls -la playwright + + - name: Install desktop dependencies + shell: bash + if: steps.cache-desktop-modules.outputs.cache-hit != 'true' + run: cd $GITHUB_WORKSPACE/desktop && yarn install --frozen-lockfile --network-timeout 600000 + + - uses: actions/cache/save@v4 + if: always() + with: + path: ${{ env.DESKTOP_CACHED_FOLDER }} + key: ${{ env.CACHE_KEY }} + + - name: Build desktop + shell: bash + run: cd $GITHUB_WORKSPACE/desktop && yarn build-everything + + - name: Install playwright dependencies + run: | + cd $GITHUB_WORKSPACE/playwright && yarn install --frozen-lockfile + + - name: Build the Desktop tests + run: | + cd $GITHUB_WORKSPACE/playwright + yarn tsc + + - name: Run the Desktop tests + run: | + cd $GITHUB_WORKSPACE/playwright + SESSION_DESKTOP_ROOT=$GITHUB_WORKSPACE/desktop nice ionice xvfb-run --auto-servernum --server-num=1 --server-args='-screen 0, 1920x1080x24' yarn test + + - name: Kill all running electron app + if: always() + continue-on-error: true # just so we don't fail + shell: bash + run: | + killall electron; diff --git a/README.md b/README.md index f05103a..9aba24b 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ This repository holds the code to do regression testing with Playwright for Sess ## Setup -`git clone https://github.com/oxen-io/session-playwright/` +`git clone https://github.com/session-foundation/session-playwright/` Install [nvm](https://github.com/nvm-sh/nvm) or [nvm for windows](https://github.com/coreybutler/nvm-windows). diff --git a/package.json b/package.json index a3c095b..89f7ef9 100644 --- a/package.json +++ b/package.json @@ -2,12 +2,13 @@ "name": "session-playwright", "version": "1.0.0", "main": "index.js", - "repository": "git@github.com:oxen-io/session-playwright.git", + "repository": "git@github.com:session-foundation/session-playwright.git", "author": "Audric Ackermann ", "license": "MIT", "devDependencies": { "@playwright/test": "^1.37.1", "@types/fs-extra": "^11.0.1", + "@types/libsodium-wrappers-sumo": "^0.7.8", "@types/lodash": "^4.14.196", "@types/uuid": "^9.0.8", "@typescript-eslint/eslint-plugin": "^6.2.1", @@ -26,7 +27,8 @@ "fs-extra": "^11.1.1", "lodash": "^4.17.21", "prettier": "^3.0.1", - "typescript": "^5.1.6", + "semver": "^7.5.4", + "typescript": "^5.5.4", "uuid": "^9.0.1" }, "scripts": { @@ -37,5 +39,9 @@ "allure-create": "allure generate ./allure-results", "allure-open": "allure open ./allure-report" }, - "dependencies": {} + "dependencies": { + "buffer-crc32": "^1.0.0", + "libsodium-wrappers-sumo": "^0.7.14", + "session-tooling": "git+https://github.com/Bi1b/session-tooling" + } } diff --git a/state_generation/actions/fetchSwarmOf.ts b/state_generation/actions/fetchSwarmOf.ts new file mode 100644 index 0000000..6733e9b --- /dev/null +++ b/state_generation/actions/fetchSwarmOf.ts @@ -0,0 +1,41 @@ +import { isEmpty, sample } from 'lodash'; +import { SwarmForSubRequest } from '../requests/snodeRequests'; +import { PubkeyType, Snode, SnodeFromSeed } from '../requests/types'; + +const fetchedSwarms: Record> = {}; + +async function getSwarmOfUser(sessionId: PubkeyType, snode: SnodeFromSeed) { + const swarmRequest = new SwarmForSubRequest(sessionId); + + const swarmResult = await fetch( + `https://${snode.public_ip}:${snode.storage_port}/storage_rpc/v1`, + { + body: JSON.stringify(await swarmRequest.build()), + method: 'POST', + }, + ); + const swarm = await swarmResult.json(); + + if (isEmpty(fetchedSwarms[sessionId])) { + fetchedSwarms[sessionId] = swarm; + } + // console.error('fetched userSwarm', swarm.snodes); + + return swarm.snodes; +} + +export async function randomSnodeOnUserSwarm( + sessionId: PubkeyType, + snode: SnodeFromSeed, +) { + const userSwarm = + fetchedSwarms[sessionId] || (await getSwarmOfUser(sessionId, snode)); + const randomSnodeOnSwarm = sample(userSwarm); + if (!randomSnodeOnSwarm) { + throw new Error(`did not find a snode for user: ${sessionId}`); + } + console.info( + `random snode for user: ${sessionId} is snode: ${randomSnodeOnSwarm.pubkey_ed25519}`, + ); + return randomSnodeOnSwarm; +} diff --git a/state_generation/index.ts b/state_generation/index.ts new file mode 100644 index 0000000..a47fb8c --- /dev/null +++ b/state_generation/index.ts @@ -0,0 +1,131 @@ +import { sample } from 'lodash'; +import { getSodiumNode, WithSodium } from '../tests/sodium'; +import { randomSnodeOnUserSwarm } from './actions/fetchSwarmOf'; + +import { getAllSnodesFromSeed } from './requests/seedRequest'; +import { PubkeyType } from './requests/types'; +import { loadSessionTools, WithSessionTools } from './sessionTools'; +import { createRandomUser, SessionUser } from './sessionUser'; + +function createRandomLegacyGroup({ sodium }: WithSodium) { + const seed = sodium.randombytes_buf(sodium.crypto_sign_ed25519_SEEDBYTES); + const ed25519KeyPair = sodium.crypto_sign_seed_keypair(seed); + const privkeyHex = sodium.to_hex(ed25519KeyPair.privateKey); + + // 64 privkey + 64 pubkey + const publicKey = sodium.crypto_sign_ed25519_sk_to_pk( + sodium.from_hex(privkeyHex), + ); + const x25519Pk = sodium.crypto_sign_ed25519_pk_to_curve25519(publicKey); + + const encKeypair = sodium.crypto_sign_keypair(); + const groupPubkey = new Uint8Array(33); + groupPubkey.set(x25519Pk, 1); + groupPubkey[0] = 5; + + const groupPk = sodium.to_hex(groupPubkey) as PubkeyType; + return { + groupPk, + encPk: encKeypair.publicKey, + encSk: encKeypair.privateKey, + }; +} +// type CreatedLegacyGroup = ReturnType; + +function makeFriendsAndKnown(...users: Array) { + if (users.length < 2) { + throw new Error('needs at least two users to make them friends'); + } + users.forEach((user1, index) => { + const user2 = users[index + 1]; + if (user2) { + user1.contacts.setApproved(user2.sessionId, true); + user1.contacts.setApprovedMe(user2.sessionId, true); + user2.contacts.setApproved(user1.sessionId, true); + user2.contacts.setApprovedMe(user1.sessionId, true); + user2.contacts.setName( + user1.sessionId, + user1.userProfile.getName() || '', + ); + user1.contacts.setName( + user2.sessionId, + user2.userProfile.getName() || '', + ); + } + }); +} + +function makeGroupWithMembers({ + members, + groupName, + sodium, +}: { + members: Array; + groupName: string; +} & WithSodium) { + // first one is the creator + if (!members.length) { + throw new Error('Excepted at least one creator/member'); + } + const [creator, ...otherMembers] = members; + const { encPk, encSk, groupPk } = createRandomLegacyGroup({ sodium }); + const legacyGroup = creator.userGroups.getOrConstructLegacyGroup(groupPk); + legacyGroup.name = groupName; + legacyGroup.encPubkey = encPk; + legacyGroup.encSeckey = encSk; + legacyGroup.insert(creator.sessionId, true); + otherMembers.forEach((member) => { + legacyGroup.insert(member.sessionId, false); // only one admin for legacy groups + }); + + [creator, ...otherMembers].forEach((member) => { + member.userGroups.setLegacyGroup(legacyGroup); + }); +} + +export function twoUsersFriends(opts: WithSodium & WithSessionTools) { + const alice = createRandomUser(opts); + const bob = createRandomUser(opts); + makeFriendsAndKnown(alice, bob); +} + +export async function prepareThreeFriendsInSharedGroup() { + const sodium = await getSodiumNode(); + const sessionTools = await loadSessionTools(); + + const alice = createRandomUser({ sodium, sessionTools }); + const bob = createRandomUser({ sodium, sessionTools }); + const charlie = createRandomUser({ sodium, sessionTools }); + const users = [alice, bob, charlie]; + + try { + alice.userProfile.setName('Alice'); + bob.userProfile.setName('Bob'); + charlie.userProfile.setName('Charlie'); + + makeFriendsAndKnown(alice, bob, charlie); + + makeGroupWithMembers({ + groupName: 'group test 1', + members: [alice, bob, charlie], + sodium, + }); + + const snodesInNetwork = await getAllSnodesFromSeed(); + + const randomSnodeFromSeed = sample(snodesInNetwork); + await Promise.all( + users.map(async (user) => { + const randomSnodeOnSwarm = await randomSnodeOnUserSwarm( + user.sessionId, + randomSnodeFromSeed, + ); + await user.pushChangesToSwarm(randomSnodeOnSwarm); + }), + ); + console.warn(`seed of alice: "${users[0].seedPhrase}"`); + return users.map((m) => ({ seed: m.seed, sessionId: m.sessionId })); + } finally { + users.map((user) => user.freeMemory()); + } +} diff --git a/state_generation/mnemonic/english.json b/state_generation/mnemonic/english.json new file mode 100644 index 0000000..f93b2c5 --- /dev/null +++ b/state_generation/mnemonic/english.json @@ -0,0 +1,1628 @@ +[ + "abbey", + "abducts", + "ability", + "ablaze", + "abnormal", + "abort", + "abrasive", + "absorb", + "abyss", + "academy", + "aces", + "aching", + "acidic", + "acoustic", + "acquire", + "across", + "actress", + "acumen", + "adapt", + "addicted", + "adept", + "adhesive", + "adjust", + "adopt", + "adrenalin", + "adult", + "adventure", + "aerial", + "afar", + "affair", + "afield", + "afloat", + "afoot", + "afraid", + "after", + "against", + "agenda", + "aggravate", + "agile", + "aglow", + "agnostic", + "agony", + "agreed", + "ahead", + "aided", + "ailments", + "aimless", + "airport", + "aisle", + "ajar", + "akin", + "alarms", + "album", + "alchemy", + "alerts", + "algebra", + "alkaline", + "alley", + "almost", + "aloof", + "alpine", + "already", + "also", + "altitude", + "alumni", + "always", + "amaze", + "ambush", + "amended", + "amidst", + "ammo", + "amnesty", + "among", + "amply", + "amused", + "anchor", + "android", + "anecdote", + "angled", + "ankle", + "annoyed", + "answers", + "antics", + "anvil", + "anxiety", + "anybody", + "apart", + "apex", + "aphid", + "aplomb", + "apology", + "apply", + "apricot", + "aptitude", + "aquarium", + "arbitrary", + "archer", + "ardent", + "arena", + "argue", + "arises", + "army", + "around", + "arrow", + "arsenic", + "artistic", + "ascend", + "ashtray", + "aside", + "asked", + "asleep", + "aspire", + "assorted", + "asylum", + "athlete", + "atlas", + "atom", + "atrium", + "attire", + "auburn", + "auctions", + "audio", + "august", + "aunt", + "austere", + "autumn", + "avatar", + "avidly", + "avoid", + "awakened", + "awesome", + "awful", + "awkward", + "awning", + "awoken", + "axes", + "axis", + "axle", + "aztec", + "azure", + "baby", + "bacon", + "badge", + "baffles", + "bagpipe", + "bailed", + "bakery", + "balding", + "bamboo", + "banjo", + "baptism", + "basin", + "batch", + "bawled", + "bays", + "because", + "beer", + "befit", + "begun", + "behind", + "being", + "below", + "bemused", + "benches", + "berries", + "bested", + "betting", + "bevel", + "beware", + "beyond", + "bias", + "bicycle", + "bids", + "bifocals", + "biggest", + "bikini", + "bimonthly", + "binocular", + "biology", + "biplane", + "birth", + "biscuit", + "bite", + "biweekly", + "blender", + "blip", + "bluntly", + "boat", + "bobsled", + "bodies", + "bogeys", + "boil", + "boldly", + "bomb", + "border", + "boss", + "both", + "bounced", + "bovine", + "bowling", + "boxes", + "boyfriend", + "broken", + "brunt", + "bubble", + "buckets", + "budget", + "buffet", + "bugs", + "building", + "bulb", + "bumper", + "bunch", + "business", + "butter", + "buying", + "buzzer", + "bygones", + "byline", + "bypass", + "cabin", + "cactus", + "cadets", + "cafe", + "cage", + "cajun", + "cake", + "calamity", + "camp", + "candy", + "casket", + "catch", + "cause", + "cavernous", + "cease", + "cedar", + "ceiling", + "cell", + "cement", + "cent", + "certain", + "chlorine", + "chrome", + "cider", + "cigar", + "cinema", + "circle", + "cistern", + "citadel", + "civilian", + "claim", + "click", + "clue", + "coal", + "cobra", + "cocoa", + "code", + "coexist", + "coffee", + "cogs", + "cohesive", + "coils", + "colony", + "comb", + "cool", + "copy", + "corrode", + "costume", + "cottage", + "cousin", + "cowl", + "criminal", + "cube", + "cucumber", + "cuddled", + "cuffs", + "cuisine", + "cunning", + "cupcake", + "custom", + "cycling", + "cylinder", + "cynical", + "dabbing", + "dads", + "daft", + "dagger", + "daily", + "damp", + "dangerous", + "dapper", + "darted", + "dash", + "dating", + "dauntless", + "dawn", + "daytime", + "dazed", + "debut", + "decay", + "dedicated", + "deepest", + "deftly", + "degrees", + "dehydrate", + "deity", + "dejected", + "delayed", + "demonstrate", + "dented", + "deodorant", + "depth", + "desk", + "devoid", + "dewdrop", + "dexterity", + "dialect", + "dice", + "diet", + "different", + "digit", + "dilute", + "dime", + "dinner", + "diode", + "diplomat", + "directed", + "distance", + "ditch", + "divers", + "dizzy", + "doctor", + "dodge", + "does", + "dogs", + "doing", + "dolphin", + "domestic", + "donuts", + "doorway", + "dormant", + "dosage", + "dotted", + "double", + "dove", + "down", + "dozen", + "dreams", + "drinks", + "drowning", + "drunk", + "drying", + "dual", + "dubbed", + "duckling", + "dude", + "duets", + "duke", + "dullness", + "dummy", + "dunes", + "duplex", + "duration", + "dusted", + "duties", + "dwarf", + "dwelt", + "dwindling", + "dying", + "dynamite", + "dyslexic", + "each", + "eagle", + "earth", + "easy", + "eating", + "eavesdrop", + "eccentric", + "echo", + "eclipse", + "economics", + "ecstatic", + "eden", + "edgy", + "edited", + "educated", + "eels", + "efficient", + "eggs", + "egotistic", + "eight", + "either", + "eject", + "elapse", + "elbow", + "eldest", + "eleven", + "elite", + "elope", + "else", + "eluded", + "emails", + "ember", + "emerge", + "emit", + "emotion", + "empty", + "emulate", + "energy", + "enforce", + "enhanced", + "enigma", + "enjoy", + "enlist", + "enmity", + "enough", + "enraged", + "ensign", + "entrance", + "envy", + "epoxy", + "equip", + "erase", + "erected", + "erosion", + "error", + "eskimos", + "espionage", + "essential", + "estate", + "etched", + "eternal", + "ethics", + "etiquette", + "evaluate", + "evenings", + "evicted", + "evolved", + "examine", + "excess", + "exhale", + "exit", + "exotic", + "exquisite", + "extra", + "exult", + "fabrics", + "factual", + "fading", + "fainted", + "faked", + "fall", + "family", + "fancy", + "farming", + "fatal", + "faulty", + "fawns", + "faxed", + "fazed", + "feast", + "february", + "federal", + "feel", + "feline", + "females", + "fences", + "ferry", + "festival", + "fetches", + "fever", + "fewest", + "fiat", + "fibula", + "fictional", + "fidget", + "fierce", + "fifteen", + "fight", + "films", + "firm", + "fishing", + "fitting", + "five", + "fixate", + "fizzle", + "fleet", + "flippant", + "flying", + "foamy", + "focus", + "foes", + "foggy", + "foiled", + "folding", + "fonts", + "foolish", + "fossil", + "fountain", + "fowls", + "foxes", + "foyer", + "framed", + "friendly", + "frown", + "fruit", + "frying", + "fudge", + "fuel", + "fugitive", + "fully", + "fuming", + "fungal", + "furnished", + "fuselage", + "future", + "fuzzy", + "gables", + "gadget", + "gags", + "gained", + "galaxy", + "gambit", + "gang", + "gasp", + "gather", + "gauze", + "gave", + "gawk", + "gaze", + "gearbox", + "gecko", + "geek", + "gels", + "gemstone", + "general", + "geometry", + "germs", + "gesture", + "getting", + "geyser", + "ghetto", + "ghost", + "giant", + "giddy", + "gifts", + "gigantic", + "gills", + "gimmick", + "ginger", + "girth", + "giving", + "glass", + "gleeful", + "glide", + "gnaw", + "gnome", + "goat", + "goblet", + "godfather", + "goes", + "goggles", + "going", + "goldfish", + "gone", + "goodbye", + "gopher", + "gorilla", + "gossip", + "gotten", + "gourmet", + "governing", + "gown", + "greater", + "grunt", + "guarded", + "guest", + "guide", + "gulp", + "gumball", + "guru", + "gusts", + "gutter", + "guys", + "gymnast", + "gypsy", + "gyrate", + "habitat", + "hacksaw", + "haggled", + "hairy", + "hamburger", + "happens", + "hashing", + "hatchet", + "haunted", + "having", + "hawk", + "haystack", + "hazard", + "hectare", + "hedgehog", + "heels", + "hefty", + "height", + "hemlock", + "hence", + "heron", + "hesitate", + "hexagon", + "hickory", + "hiding", + "highway", + "hijack", + "hiker", + "hills", + "himself", + "hinder", + "hippo", + "hire", + "history", + "hitched", + "hive", + "hoax", + "hobby", + "hockey", + "hoisting", + "hold", + "honked", + "hookup", + "hope", + "hornet", + "hospital", + "hotel", + "hounded", + "hover", + "howls", + "hubcaps", + "huddle", + "huge", + "hull", + "humid", + "hunter", + "hurried", + "husband", + "huts", + "hybrid", + "hydrogen", + "hyper", + "iceberg", + "icing", + "icon", + "identity", + "idiom", + "idled", + "idols", + "igloo", + "ignore", + "iguana", + "illness", + "imagine", + "imbalance", + "imitate", + "impel", + "inactive", + "inbound", + "incur", + "industrial", + "inexact", + "inflamed", + "ingested", + "initiate", + "injury", + "inkling", + "inline", + "inmate", + "innocent", + "inorganic", + "input", + "inquest", + "inroads", + "insult", + "intended", + "inundate", + "invoke", + "inwardly", + "ionic", + "irate", + "iris", + "irony", + "irritate", + "island", + "isolated", + "issued", + "italics", + "itches", + "items", + "itinerary", + "itself", + "ivory", + "jabbed", + "jackets", + "jaded", + "jagged", + "jailed", + "jamming", + "january", + "jargon", + "jaunt", + "javelin", + "jaws", + "jazz", + "jeans", + "jeers", + "jellyfish", + "jeopardy", + "jerseys", + "jester", + "jetting", + "jewels", + "jigsaw", + "jingle", + "jittery", + "jive", + "jobs", + "jockey", + "jogger", + "joining", + "joking", + "jolted", + "jostle", + "journal", + "joyous", + "jubilee", + "judge", + "juggled", + "juicy", + "jukebox", + "july", + "jump", + "junk", + "jury", + "justice", + "juvenile", + "kangaroo", + "karate", + "keep", + "kennel", + "kept", + "kernels", + "kettle", + "keyboard", + "kickoff", + "kidneys", + "king", + "kiosk", + "kisses", + "kitchens", + "kiwi", + "knapsack", + "knee", + "knife", + "knowledge", + "knuckle", + "koala", + "laboratory", + "ladder", + "lagoon", + "lair", + "lakes", + "lamb", + "language", + "laptop", + "large", + "last", + "later", + "launching", + "lava", + "lawsuit", + "layout", + "lazy", + "lectures", + "ledge", + "leech", + "left", + "legion", + "leisure", + "lemon", + "lending", + "leopard", + "lesson", + "lettuce", + "lexicon", + "liar", + "library", + "licks", + "lids", + "lied", + "lifestyle", + "light", + "likewise", + "lilac", + "limits", + "linen", + "lion", + "lipstick", + "liquid", + "listen", + "lively", + "loaded", + "lobster", + "locker", + "lodge", + "lofty", + "logic", + "loincloth", + "long", + "looking", + "lopped", + "lordship", + "losing", + "lottery", + "loudly", + "love", + "lower", + "loyal", + "lucky", + "luggage", + "lukewarm", + "lullaby", + "lumber", + "lunar", + "lurk", + "lush", + "luxury", + "lymph", + "lynx", + "lyrics", + "macro", + "madness", + "magically", + "mailed", + "major", + "makeup", + "malady", + "mammal", + "maps", + "masterful", + "match", + "maul", + "maverick", + "maximum", + "mayor", + "maze", + "meant", + "mechanic", + "medicate", + "meeting", + "megabyte", + "melting", + "memoir", + "menu", + "merger", + "mesh", + "metro", + "mews", + "mice", + "midst", + "mighty", + "mime", + "mirror", + "misery", + "mittens", + "mixture", + "moat", + "mobile", + "mocked", + "mohawk", + "moisture", + "molten", + "moment", + "money", + "moon", + "mops", + "morsel", + "mostly", + "motherly", + "mouth", + "movement", + "mowing", + "much", + "muddy", + "muffin", + "mugged", + "mullet", + "mumble", + "mundane", + "muppet", + "mural", + "musical", + "muzzle", + "myriad", + "mystery", + "myth", + "nabbing", + "nagged", + "nail", + "names", + "nanny", + "napkin", + "narrate", + "nasty", + "natural", + "nautical", + "navy", + "nearby", + "necklace", + "needed", + "negative", + "neither", + "neon", + "nephew", + "nerves", + "nestle", + "network", + "neutral", + "never", + "newt", + "nexus", + "nibs", + "niche", + "niece", + "nifty", + "nightly", + "nimbly", + "nineteen", + "nirvana", + "nitrogen", + "nobody", + "nocturnal", + "nodes", + "noises", + "nomad", + "noodles", + "northern", + "nostril", + "noted", + "nouns", + "novelty", + "nowhere", + "nozzle", + "nuance", + "nucleus", + "nudged", + "nugget", + "nuisance", + "null", + "number", + "nuns", + "nurse", + "nutshell", + "nylon", + "oaks", + "oars", + "oasis", + "oatmeal", + "obedient", + "object", + "obliged", + "obnoxious", + "observant", + "obtains", + "obvious", + "occur", + "ocean", + "october", + "odds", + "odometer", + "offend", + "often", + "oilfield", + "ointment", + "okay", + "older", + "olive", + "olympics", + "omega", + "omission", + "omnibus", + "onboard", + "oncoming", + "oneself", + "ongoing", + "onion", + "online", + "onslaught", + "onto", + "onward", + "oozed", + "opacity", + "opened", + "opposite", + "optical", + "opus", + "orange", + "orbit", + "orchid", + "orders", + "organs", + "origin", + "ornament", + "orphans", + "oscar", + "ostrich", + "otherwise", + "otter", + "ouch", + "ought", + "ounce", + "ourselves", + "oust", + "outbreak", + "oval", + "oven", + "owed", + "owls", + "owner", + "oxidant", + "oxygen", + "oyster", + "ozone", + "pact", + "paddles", + "pager", + "pairing", + "palace", + "pamphlet", + "pancakes", + "paper", + "paradise", + "pastry", + "patio", + "pause", + "pavements", + "pawnshop", + "payment", + "peaches", + "pebbles", + "peculiar", + "pedantic", + "peeled", + "pegs", + "pelican", + "pencil", + "people", + "pepper", + "perfect", + "pests", + "petals", + "phase", + "pheasants", + "phone", + "phrases", + "physics", + "piano", + "picked", + "pierce", + "pigment", + "piloted", + "pimple", + "pinched", + "pioneer", + "pipeline", + "pirate", + "pistons", + "pitched", + "pivot", + "pixels", + "pizza", + "playful", + "pledge", + "pliers", + "plotting", + "plus", + "plywood", + "poaching", + "pockets", + "podcast", + "poetry", + "point", + "poker", + "polar", + "ponies", + "pool", + "popular", + "portents", + "possible", + "potato", + "pouch", + "poverty", + "powder", + "pram", + "present", + "pride", + "problems", + "pruned", + "prying", + "psychic", + "public", + "puck", + "puddle", + "puffin", + "pulp", + "pumpkins", + "punch", + "puppy", + "purged", + "push", + "putty", + "puzzled", + "pylons", + "pyramid", + "python", + "queen", + "quick", + "quote", + "rabbits", + "racetrack", + "radar", + "rafts", + "rage", + "railway", + "raking", + "rally", + "ramped", + "randomly", + "rapid", + "rarest", + "rash", + "rated", + "ravine", + "rays", + "razor", + "react", + "rebel", + "recipe", + "reduce", + "reef", + "refer", + "regular", + "reheat", + "reinvest", + "rejoices", + "rekindle", + "relic", + "remedy", + "renting", + "reorder", + "repent", + "request", + "reruns", + "rest", + "return", + "reunion", + "revamp", + "rewind", + "rhino", + "rhythm", + "ribbon", + "richly", + "ridges", + "rift", + "rigid", + "rims", + "ringing", + "riots", + "ripped", + "rising", + "ritual", + "river", + "roared", + "robot", + "rockets", + "rodent", + "rogue", + "roles", + "romance", + "roomy", + "roped", + "roster", + "rotate", + "rounded", + "rover", + "rowboat", + "royal", + "ruby", + "rudely", + "ruffled", + "rugged", + "ruined", + "ruling", + "rumble", + "runway", + "rural", + "rustled", + "ruthless", + "sabotage", + "sack", + "sadness", + "safety", + "saga", + "sailor", + "sake", + "salads", + "sample", + "sanity", + "sapling", + "sarcasm", + "sash", + "satin", + "saucepan", + "saved", + "sawmill", + "saxophone", + "sayings", + "scamper", + "scenic", + "school", + "science", + "scoop", + "scrub", + "scuba", + "seasons", + "second", + "sedan", + "seeded", + "segments", + "seismic", + "selfish", + "semifinal", + "sensible", + "september", + "sequence", + "serving", + "session", + "setup", + "seventh", + "sewage", + "shackles", + "shelter", + "shipped", + "shocking", + "shrugged", + "shuffled", + "shyness", + "siblings", + "sickness", + "sidekick", + "sieve", + "sifting", + "sighting", + "silk", + "simplest", + "sincerely", + "sipped", + "siren", + "situated", + "sixteen", + "sizes", + "skater", + "skew", + "skirting", + "skulls", + "skydive", + "slackens", + "sleepless", + "slid", + "slower", + "slug", + "smash", + "smelting", + "smidgen", + "smog", + "smuggled", + "snake", + "sneeze", + "sniff", + "snout", + "snug", + "soapy", + "sober", + "soccer", + "soda", + "software", + "soggy", + "soil", + "solved", + "somewhere", + "sonic", + "soothe", + "soprano", + "sorry", + "southern", + "sovereign", + "sowed", + "soya", + "space", + "speedy", + "sphere", + "spiders", + "splendid", + "spout", + "sprig", + "spud", + "spying", + "square", + "stacking", + "stellar", + "stick", + "stockpile", + "strained", + "stunning", + "stylishly", + "subtly", + "succeed", + "suddenly", + "suede", + "suffice", + "sugar", + "suitcase", + "sulking", + "summon", + "sunken", + "superior", + "surfer", + "sushi", + "suture", + "swagger", + "swept", + "swiftly", + "sword", + "swung", + "syllabus", + "symptoms", + "syndrome", + "syringe", + "system", + "taboo", + "tacit", + "tadpoles", + "tagged", + "tail", + "taken", + "talent", + "tamper", + "tanks", + "tapestry", + "tarnished", + "tasked", + "tattoo", + "taunts", + "tavern", + "tawny", + "taxi", + "teardrop", + "technical", + "tedious", + "teeming", + "tell", + "template", + "tender", + "tepid", + "tequila", + "terminal", + "testing", + "tether", + "textbook", + "thaw", + "theatrics", + "thirsty", + "thorn", + "threaten", + "thumbs", + "thwart", + "ticket", + "tidy", + "tiers", + "tiger", + "tilt", + "timber", + "tinted", + "tipsy", + "tirade", + "tissue", + "titans", + "toaster", + "tobacco", + "today", + "toenail", + "toffee", + "together", + "toilet", + "token", + "tolerant", + "tomorrow", + "tonic", + "toolbox", + "topic", + "torch", + "tossed", + "total", + "touchy", + "towel", + "toxic", + "toyed", + "trash", + "trendy", + "tribal", + "trolling", + "truth", + "trying", + "tsunami", + "tubes", + "tucks", + "tudor", + "tuesday", + "tufts", + "tugs", + "tuition", + "tulips", + "tumbling", + "tunnel", + "turnip", + "tusks", + "tutor", + "tuxedo", + "twang", + "tweezers", + "twice", + "twofold", + "tycoon", + "typist", + "tyrant", + "ugly", + "ulcers", + "ultimate", + "umbrella", + "umpire", + "unafraid", + "unbending", + "uncle", + "under", + "uneven", + "unfit", + "ungainly", + "unhappy", + "union", + "unjustly", + "unknown", + "unlikely", + "unmask", + "unnoticed", + "unopened", + "unplugs", + "unquoted", + "unrest", + "unsafe", + "until", + "unusual", + "unveil", + "unwind", + "unzip", + "upbeat", + "upcoming", + "update", + "upgrade", + "uphill", + "upkeep", + "upload", + "upon", + "upper", + "upright", + "upstairs", + "uptight", + "upwards", + "urban", + "urchins", + "urgent", + "usage", + "useful", + "usher", + "using", + "usual", + "utensils", + "utility", + "utmost", + "utopia", + "uttered", + "vacation", + "vague", + "vain", + "value", + "vampire", + "vane", + "vapidly", + "vary", + "vastness", + "vats", + "vaults", + "vector", + "veered", + "vegan", + "vehicle", + "vein", + "velvet", + "venomous", + "verification", + "vessel", + "veteran", + "vexed", + "vials", + "vibrate", + "victim", + "video", + "viewpoint", + "vigilant", + "viking", + "village", + "vinegar", + "violin", + "vipers", + "virtual", + "visited", + "vitals", + "vivid", + "vixen", + "vocal", + "vogue", + "voice", + "volcano", + "vortex", + "voted", + "voucher", + "vowels", + "voyage", + "vulture", + "wade", + "waffle", + "wagtail", + "waist", + "waking", + "wallets", + "wanted", + "warped", + "washing", + "water", + "waveform", + "waxing", + "wayside", + "weavers", + "website", + "wedge", + "weekday", + "weird", + "welders", + "went", + "wept", + "were", + "western", + "wetsuit", + "whale", + "when", + "whipped", + "whole", + "wickets", + "width", + "wield", + "wife", + "wiggle", + "wildly", + "winter", + "wipeout", + "wiring", + "wise", + "withdrawn", + "wives", + "wizard", + "wobbly", + "woes", + "woken", + "wolf", + "womanly", + "wonders", + "woozy", + "worry", + "wounded", + "woven", + "wrap", + "wrist", + "wrong", + "yacht", + "yahoo", + "yanks", + "yard", + "yawning", + "yearbook", + "yellow", + "yesterday", + "yeti", + "yields", + "yodel", + "yoga", + "younger", + "yoyo", + "zapped", + "zeal", + "zebra", + "zero", + "zesty", + "zigzags", + "zinger", + "zippers", + "zodiac", + "zombie", + "zones", + "zoom" +] diff --git a/state_generation/mnemonic/mnemonic.ts b/state_generation/mnemonic/mnemonic.ts new file mode 100644 index 0000000..2de252a --- /dev/null +++ b/state_generation/mnemonic/mnemonic.ts @@ -0,0 +1,142 @@ +import crc32 from 'buffer-crc32'; + +const MN_DEFAULT_WORDSET = 'english'; + +function mn_get_checksum_index(words: Array, prefixLen: number) { + let trimmedWords = ''; + + for (let i = 0; i < words.length; i++) { + trimmedWords += words[i].slice(0, prefixLen); + } + const checksum = crc32.unsigned(trimmedWords as any); + const index = checksum % words.length; + return index; +} + +function mn_swap_endian_4byte(str: string) { + if (str.length !== 8) { + throw new Error(`Invalid input length: ${str.length}`); + } + return str.slice(6, 8) + str.slice(4, 6) + str.slice(2, 4) + str.slice(0, 2); +} + +export function mnEncode( + str: string, + wordsetName: string = MN_DEFAULT_WORDSET, +): string { + const wordset = mnWords[wordsetName]; + let out = [] as Array; + const n = wordset.words.length; + let strCopy = str; + for (let j = 0; j < strCopy.length; j += 8) { + strCopy = + strCopy.slice(0, j) + + mn_swap_endian_4byte(strCopy.slice(j, j + 8)) + + strCopy.slice(j + 8); + } + for (let i = 0; i < strCopy.length; i += 8) { + const x = parseInt(strCopy.substr(i, 8), 16); + const w1 = x % n; + const w2 = (Math.floor(x / n) + w1) % n; + const w3 = (Math.floor(Math.floor(x / n) / n) + w2) % n; + out = out.concat([wordset.words[w1], wordset.words[w2], wordset.words[w3]]); + } + if (wordset.prefixLen > 0) { + out.push(out[mn_get_checksum_index(out, wordset.prefixLen)]); + } + return out.join(' '); +} + +export function mnDecode(seedPhrase: string): string { + const wordset = mnWords[MN_DEFAULT_WORDSET]; + let out = ''; + const n = wordset.words.length; + const wlist = seedPhrase.split(' '); + let checksumWord = ''; + if (wlist.length < 12) { + throw new Error("You've entered too few words, please try again"); + } + if ( + (wordset.prefixLen === 0 && wlist.length % 3 !== 0) || + (wordset.prefixLen > 0 && wlist.length % 3 === 2) + ) { + throw new Error("You've entered too few words, please try again"); + } + if (wordset.prefixLen > 0 && wlist.length % 3 === 0) { + throw new Error( + 'You seem to be missing the last word in your private key, please try again', + ); + } + if (wordset.prefixLen > 0) { + // Pop checksum from mnemonic + checksumWord = wlist.pop() as string; + } + // Decode mnemonic + for (let i = 0; i < wlist.length; i += 3) { + let w1; + let w2; + let w3; + if (wordset.prefixLen === 0) { + w1 = wordset.words.indexOf(wlist[i]); + w2 = wordset.words.indexOf(wlist[i + 1]); + w3 = wordset.words.indexOf(wlist[i + 2]); + } else { + w1 = wordset.truncWords.indexOf(wlist[i].slice(0, wordset.prefixLen)); + w2 = wordset.truncWords.indexOf(wlist[i + 1].slice(0, wordset.prefixLen)); + w3 = wordset.truncWords.indexOf(wlist[i + 2].slice(0, wordset.prefixLen)); + } + if (w1 === -1 || w2 === -1 || w3 === -1) { + throw new Error('invalid word in mnemonic'); + } + + const x = w1 + n * ((n - w1 + w2) % n) + n * n * ((n - w2 + w3) % n); + if (x % n !== w1) { + throw new Error( + 'Something went wrong when decoding your private key, please try again', + ); + } + out += mn_swap_endian_4byte(`0000000${x.toString(16)}`.slice(-8)); + } + // Verify checksum + if (wordset.prefixLen > 0) { + const index = mn_get_checksum_index(wlist, wordset.prefixLen); + const expectedChecksumWord = wlist[index]; + if ( + expectedChecksumWord.slice(0, wordset.prefixLen) !== + checksumWord.slice(0, wordset.prefixLen) + ) { + throw new Error( + 'Your private key could not be verified, please verify the checksum word', + ); + } + } + return out; +} + +const mnWords = {} as Record< + string, + { + prefixLen: number; + words: any; + truncWords: Array; + } +>; +mnWords.english = { + prefixLen: 3, + // eslint-disable-next-line global-require + words: require('./english.json'), + truncWords: [], +}; + +for (const i in mnWords) { + if (mnWords.hasOwnProperty(i)) { + if (mnWords[i].prefixLen === 0) { + continue; + } + for (let j = 0; j < mnWords[i].words.length; ++j) { + mnWords[i].truncWords.push( + mnWords[i].words[j].slice(0, mnWords[i].prefixLen), + ); + } + } +} diff --git a/state_generation/requests/seedRequest.ts b/state_generation/requests/seedRequest.ts new file mode 100644 index 0000000..d001f62 --- /dev/null +++ b/state_generation/requests/seedRequest.ts @@ -0,0 +1,47 @@ +const testNet = true; + +const seedNode = testNet + ? { ip: 'seed2.getsession.org', port: 38157 } + : { ip: 'storage.seed1.loki.network', port: 4433 }; + +export async function getAllSnodesFromSeed() { + const getAll = new GetSnodesFromSeed(); + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; + try { + const result = await fetch( + `http${testNet ? '' : 's'}://${seedNode.ip}:${seedNode.port}/json_rpc`, + { + body: JSON.stringify(getAll.build()), + method: 'POST', + }, + ); + + const json = await result.json(); + console.warn('json', json); + + return json.result.service_node_states; + } catch (e) { + console.warn(e); + throw e; + } +} + +class GetSnodesFromSeed { + public build() { + return { + jsonrpc: '2.0', + id: '0', + method: 'get_n_service_nodes', + params: { + active_only: true, + limit: 20, + fields: { + public_ip: true, + storage_port: true, + pubkey_x25519: true, + pubkey_ed25519: true, + }, + }, + }; + } +} diff --git a/state_generation/requests/snodeRequests.ts b/state_generation/requests/snodeRequests.ts new file mode 100644 index 0000000..4d3f27b --- /dev/null +++ b/state_generation/requests/snodeRequests.ts @@ -0,0 +1,152 @@ +import { base64_variants, to_base64 } from 'libsodium-wrappers-sumo'; +import { isEmpty } from 'lodash'; +import { UserSigner } from '../signer/userSigner'; +import { GroupPubkeyType, PubkeyType, WithGetNow } from './types'; + +export class SwarmForSubRequest { + public method = 'get_swarm' as const; + public readonly pubkey; + + constructor(pubkey: PubkeyType | GroupPubkeyType) { + this.pubkey = pubkey; + } + + public async build() { + return { + method: this.method, + params: { + pubkey: this.pubkey, + params: { + active_only: true, + fields: { + public_ip: true, + storage_port: true, + pubkey_x25519: true, + pubkey_ed25519: true, + }, + }, + }, + } as const; + } + + public loggingId(): string { + return `${this.method}`; + } +} + +export class StoreUserSubRequest { + public method = 'store' as const; + public readonly namespace: number; + private readonly signer: UserSigner; + getNow: () => number; + + constructor({ + namespace, + userSigner, + getNow, + }: WithGetNow & { + namespace: number; + userSigner: UserSigner; + }) { + this.namespace = namespace; + this.signer = userSigner; + this.getNow = getNow; + } + + public async build() { + const { pubkey, pubkey_ed25519, signature, timestamp } = + await this.signer.getSnodeSignatureParams({ + method: this.method, + namespace: this.namespace, + getNow: this.getNow, + }); + + return { + method: this.method, + params: { + namespace: this.namespace, + pubkey, + pubkey_ed25519, + signature, + timestamp, // we give a timestamp to force verification of the signature provided + }, + }; + } + + public async toBody(): Promise { + return JSON.stringify(await this.build()); + } + public loggingId(): string { + return `${this.method}`; + } +} + +export class StoreUserConfigSubRequest { + public method = 'store' as const; + public readonly namespace: number; + public readonly ttlMs: number; + public readonly encryptedData: Uint8Array; + public readonly destination: PubkeyType; + public readonly userSigner: UserSigner; + + constructor(args: { + namespace: number; + ttlMs: number; + encryptedData: Uint8Array; + sessionId: PubkeyType; + userSigner: UserSigner; + }) { + this.namespace = args.namespace; + this.ttlMs = args.ttlMs; + this.encryptedData = args.encryptedData; + this.destination = args.sessionId; + this.userSigner = args.userSigner; + + if (isEmpty(this.encryptedData)) { + throw new Error('this.encryptedData cannot be empty'); + } + + if (isEmpty(this.destination)) { + throw new Error('this.destination cannot be empty'); + } + } + + public async build() { + const encryptedDataBase64 = to_base64( + this.encryptedData, + base64_variants.ORIGINAL, + ); + + const signDetails = await this.userSigner.getSnodeSignatureParams({ + getNow: () => Date.now(), + method: this.method, + namespace: this.namespace, + }); + + if (!signDetails) { + throw new Error( + `[StoreUserConfigSubRequest] signing returned an empty result`, + ); + } + + const toRet = { + method: this.method, + params: { + namespace: this.namespace, + ttl: this.ttlMs, + data: encryptedDataBase64, + ...signDetails, + }, + }; + + return toRet; + } + + public loggingId(): string { + return `${this.method}-${this.destination}-${this.namespace}`; + } + + public getDestination() { + return this.destination; + } +} diff --git a/state_generation/requests/types.ts b/state_generation/requests/types.ts new file mode 100644 index 0000000..b1d5fbd --- /dev/null +++ b/state_generation/requests/types.ts @@ -0,0 +1,34 @@ +const prefixStandard = '05' as const; +const prefixGroup = '03' as const; + +export type GroupPubkeyType = `${typeof prefixGroup}${string}`; +export type PubkeyType = `${typeof prefixStandard}${string}`; + +export type SnodeSigParamsShared = { + namespace: number | null | 'all'; // 'all' can be used to clear all namespaces (during account deletion) + method: 'retrieve' | 'store' | 'delete_all'; +}; + +export type SnodeSigParamsUs = SnodeSigParamsShared & { + pubKey: string; + privKey: Uint8Array; // len 64 +}; +export type WithGetNow = { getNow: () => number }; +export type WithTimestamp = { timestamp: number }; +export type WithSignature = { signature: string }; +export type SnodeSignatureResult = WithSignature & + WithTimestamp & { + pubkey_ed25519: string; + pubkey: string; // this is the x25519 key of the pubkey we are doing the request to (ourself for our swarm usually) + }; + +export type SnodeFromSeed = { + storage_port: number; + public_ip: string; +}; + +export type Snode = { + port: number; + ip: string; + pubkey_ed25519: string; +} \ No newline at end of file diff --git a/state_generation/sessionTools/index.ts b/state_generation/sessionTools/index.ts new file mode 100644 index 0000000..5e142c7 --- /dev/null +++ b/state_generation/sessionTools/index.ts @@ -0,0 +1,10 @@ +import sessionToolsPromise, { MainModule } from 'session-tooling'; + +export async function loadSessionTools() { + const loaded = await sessionToolsPromise(); + return loaded as MainModule; +} + +export type WithSessionTools = { + sessionTools: MainModule; +}; diff --git a/state_generation/sessionUser.ts b/state_generation/sessionUser.ts new file mode 100644 index 0000000..e7da60f --- /dev/null +++ b/state_generation/sessionUser.ts @@ -0,0 +1,114 @@ +import type { Contacts, UserGroups, UserProfile } from 'session-tooling'; +import { WithSodium } from '../tests/sodium'; +import { PubkeyType, Snode } from './requests/types'; +import { StoreUserConfigSubRequest } from './requests/snodeRequests'; +import { from_hex, to_hex } from 'libsodium-wrappers-sumo'; +import { UserSigner } from './signer/userSigner'; +import { WithSessionTools } from './sessionTools'; +import { mnEncode } from './mnemonic/mnemonic'; + +function buildUserSigner(user: SessionUser) { + const userSigner = new UserSigner({ + sessionId: user.sessionId, + ed25519PrivKey: user.ed25519Sk, + ed25519PubKey: to_hex(user.ed25519Pk), + }); + return userSigner; +} + +export class SessionUser { + public readonly sessionId: PubkeyType; + public readonly ed25519Pk: Uint8Array; + public readonly ed25519Sk: Uint8Array; + public readonly seed: Uint8Array; + public readonly seedPhrase: string; + public readonly wrappers: Array; + public readonly userProfile: UserProfile; + public readonly contacts: Contacts; + public readonly userGroups: UserGroups; + public readonly userSigner: UserSigner; + + constructor( + { sessionTools, sodium }: WithSessionTools & WithSodium, + seed: Uint8Array, + ) { + const ed25519KeyPair = sodium.crypto_sign_seed_keypair(seed); + const privkeyHex = sodium.to_hex(ed25519KeyPair.privateKey); + + // 64 privkey + 64 pubkey + const publicKey = sodium.crypto_sign_ed25519_sk_to_pk( + sodium.from_hex(privkeyHex), + ); + const x25519PublicKey = + sodium.crypto_sign_ed25519_pk_to_curve25519(publicKey); + + const sessId = new Uint8Array(33); + sessId.set(x25519PublicKey, 1); + sessId[0] = 5; + const sessionId = sodium.to_hex(sessId) as PubkeyType; + const userProfile = new sessionTools.UserProfile( + ed25519KeyPair.privateKey, + undefined, + ); + const contacts = new sessionTools.Contacts( + ed25519KeyPair.privateKey, + undefined, + ); + const userGroups = new sessionTools.UserGroups( + ed25519KeyPair.privateKey, + undefined, + ); + const wrappers = [userProfile, contacts, userGroups]; + + this.sessionId = sessionId; + this.ed25519Pk = ed25519KeyPair.publicKey; + this.ed25519Sk = ed25519KeyPair.privateKey; + this.seed = seed; + this.seedPhrase = mnEncode(to_hex(seed)); + this.wrappers = wrappers; + this.userProfile = userProfile; + this.contacts = contacts; + this.userGroups = userGroups; + this.userSigner = buildUserSigner(this); + } + + public async pushChangesToSwarm(snode: Snode) { + const storeRequests = this.wrappers.map( + (wrapper) => + new StoreUserConfigSubRequest({ + namespace: wrapper.storageNamespace().value, + encryptedData: from_hex(wrapper.makePushHex()), + sessionId: this.sessionId, + ttlMs: 3600 * 24, // 1 day should be enough for testing and debugging a test? + userSigner: this.userSigner, + }), + ); + + const storeStatus = await Promise.all( + storeRequests.map(async (request) => { + const builtRequest = await request.build(); + const ret = await fetch( + `https://${snode.ip}:${snode.port}/storage_rpc/v1`, + { + body: JSON.stringify(builtRequest), + method: 'POST', + }, + ); + return ret.status; + }), + ); + console.warn(`storeStatus for ${this.userProfile.getName()}:`, storeStatus); + } + + public freeMemory() { + this.wrappers.map((wrapper) => wrapper.delete()); + } +} + +export function createRandomUser(details: WithSodium & WithSessionTools) { + const seed = details.sodium.randombytes_buf( + details.sodium.crypto_sign_ed25519_SEEDBYTES, + ); + + return new SessionUser(details, seed); +} diff --git a/state_generation/signer/userSigner.ts b/state_generation/signer/userSigner.ts new file mode 100644 index 0000000..8eb26e8 --- /dev/null +++ b/state_generation/signer/userSigner.ts @@ -0,0 +1,102 @@ +import { + base64_variants, + from_string, + to_base64, +} from 'libsodium-wrappers-sumo'; +import { getSodiumNode } from '../../tests/sodium'; +import { + PubkeyType, + SnodeSignatureResult, + SnodeSigParamsShared, + SnodeSigParamsUs, + WithGetNow, +} from '../requests/types'; + +export function getVerificationDataForStoreRetrieve( + params: SnodeSigParamsShared & WithGetNow, +) { + const signatureTimestamp = params.getNow(); + const verificationString = `${params.method}${ + params.namespace === 0 ? '' : params.namespace + }${signatureTimestamp}`; + const verificationData = from_string(verificationString); + return { + toSign: new Uint8Array(verificationData), + signatureTimestamp, + }; +} + +async function getSnodeSignatureShared(params: SnodeSigParamsUs & WithGetNow) { + const { signatureTimestamp, toSign } = + getVerificationDataForStoreRetrieve(params); + + try { + const sodium = await getSodiumNode(); + + const signature = sodium.crypto_sign_detached(toSign, params.privKey); + const signatureBase64 = to_base64(signature, base64_variants.ORIGINAL); + + return { + timestamp: signatureTimestamp, + signature: signatureBase64, + }; + } catch (e: any) { + throw e; + } +} + +export class UserSigner { + private readonly ed25519PubKey: string; + private readonly ed25519PrivKey: Uint8Array; + public readonly sessionId: PubkeyType; + + constructor({ + ed25519PrivKey, + ed25519PubKey, + sessionId, + }: { + ed25519PubKey: string; + ed25519PrivKey: Uint8Array; + sessionId: PubkeyType; + }) { + this.ed25519PubKey = ed25519PubKey; + if (this.ed25519PubKey.length !== 64) { + console.warn('ed25519PubKey length', ed25519PubKey.length); + throw new Error('ed25519PubKey not 64 long'); + } + this.ed25519PrivKey = ed25519PrivKey; + if (this.ed25519PrivKey.length !== 64) { + console.warn('ed25519PrivKey length', ed25519PrivKey.length); + throw new Error('ed25519PrivKey not 64 long'); + } + this.sessionId = sessionId; + } + + async getSnodeSignatureParams({ + method, + namespace, + getNow, + }: Pick & + WithGetNow): Promise { + if (!this.ed25519PrivKey || !this.ed25519PubKey) { + const err = `getSnodeSignatureParams "${method}": User has no getUserED25519KeyPairBytes()`; + throw new Error(err); + } + + const sigData = await getSnodeSignatureShared({ + pubKey: this.sessionId, + method, + namespace, + privKey: this.ed25519PrivKey, + getNow, + }); + + return { + ...sigData, + pubkey_ed25519: this.ed25519PubKey, + pubkey: this.sessionId, + }; + } +} + + diff --git a/state_generation/types.ts b/state_generation/types.ts new file mode 100644 index 0000000..e69de29 diff --git a/tests/automation/message_checks.spec.ts b/tests/automation/message_checks.spec.ts index 8117f7d..bd6cf3b 100644 --- a/tests/automation/message_checks.spec.ts +++ b/tests/automation/message_checks.spec.ts @@ -386,76 +386,3 @@ sessionTestTwoWindows( console.log(timesArray); }, ); - -// *************** NEED TO WAIT FOR LINK PREVIEW FIX ************************************************* - -test_Alice_1W_Bob_1W( - 'Send link 1:1', - async ({ alice, aliceWindow1, bob, bobWindow1 }) => { - const testMessage = 'https://example.net'; - const testReply = `${bob.userName} replying to link from ${alice.userName}`; - - await createContact(aliceWindow1, bobWindow1, alice, bob); - - await typeIntoInput(aliceWindow1, 'message-input-text-area', testMessage); - await sleepFor(5000); - await clickOnElement({ - window: aliceWindow1, - strategy: 'data-testid', - selector: 'send-message-button', - }); - await sleepFor(1000); - await replyTo({ - senderWindow: bobWindow1, - textMessage: testMessage, - replyText: testReply, - receiverWindow: aliceWindow1, - }); - }, -); - -test_Alice_1W_Bob_1W( - 'Send community invite', - async ({ alice, aliceWindow1, bob, bobWindow1 }) => { - await createContact(aliceWindow1, bobWindow1, alice, bob); - await joinCommunity(aliceWindow1); - await clickOnTestIdWithText(aliceWindow1, 'conversation-options-avatar'); - await clickOnTestIdWithText(aliceWindow1, 'add-user-button'); - // Implementing in groups rebuild - // await waitForTestIdWithText( - // aliceWindow1, - // 'modal-heading', - // englishStrippedStr('membersInvite').toString(), - // ); - // await clickOnTestIdWithText(aliceWindow1, 'contact', bob.userName); - await clickOnMatchingText(aliceWindow1, bob.userName); - // await clickOnTestIdWithText(aliceWindow1, 'session-confirm-ok-button'); - await clickOnMatchingText( - aliceWindow1, - englishStrippedStr('okay').toString(), - ); - // Implementing in groups rebuild - // await clickOnTestIdWithText(aliceWindow1, 'modal-close-button'); - await clickOnTestIdWithText( - aliceWindow1, - 'module-conversation__user__profile-name', - bob.userName, - ); - await Promise.all([ - waitForElement( - aliceWindow1, - 'class', - 'group-name', - undefined, - testCommunityName, - ), - waitForElement( - bobWindow1, - 'class', - 'group-name', - undefined, - testCommunityName, - ), - ]); - }, -); diff --git a/tests/automation/setup/open.ts b/tests/automation/setup/open.ts index 42b2f40..dcb5282 100644 --- a/tests/automation/setup/open.ts +++ b/tests/automation/setup/open.ts @@ -18,14 +18,14 @@ export function getAppRootPath() { return process.env.SESSION_DESKTOP_ROOT as string; } -function getSessionDesktopBinPath() { - if (isEmpty(process.env.SESSION_DESKTOP_BIN)) { - throw new Error( - "You need to set the 'SESSION_DESKTOP_BIN' env variable to the session-desktop bin you want to test first (maybe `/usr/bin/session-desktop` ?)", - ); - } - return process.env.SESSION_DESKTOP_BIN as string; -} +// function getSessionDesktopBinPath() { +// if (isEmpty(process.env.SESSION_DESKTOP_BIN)) { +// throw new Error( +// "You need to set the 'SESSION_DESKTOP_BIN' env variable to the session-desktop bin you want to test first (maybe `/usr/bin/session-desktop` ?)", +// ); +// } +// return process.env.SESSION_DESKTOP_BIN as string; +// } export async function openApp(windowsToCreate: number) { if (windowsToCreate >= multisAvailable.length) { @@ -56,20 +56,20 @@ const openElectronAppOnly = async (multi: string) => { process.env.NODE_APP_INSTANCE = `${MULTI_PREFIX}-devprod-${uniqueId}-${process.env.MULTI}`; process.env.NODE_ENV = NODE_ENV; - if (!isEmpty(process.env.CI)) { - const sessionBinPath = getSessionDesktopBinPath(); - const fakeHome = `/tmp/${process.env.NODE_APP_INSTANCE}`; + // if (!isEmpty(process.env.CI)) { + // const sessionBinPath = getSessionDesktopBinPath(); + // const fakeHome = `/tmp/${process.env.NODE_APP_INSTANCE}`; - console.info(` CI RUN`); - console.info(` SESSION_BIN_PATH=${sessionBinPath}`); - console.info(` HOME="${fakeHome}"`); + // console.info(` CI RUN`); + // console.info(` SESSION_BIN_PATH=${sessionBinPath}`); + // console.info(` HOME="${fakeHome}"`); - process.env.HOME = fakeHome; + // process.env.HOME = fakeHome; - return electron.launch({ - executablePath: sessionBinPath, - }); - } + // return electron.launch({ + // executablePath: sessionBinPath, + // }); + // } console.info(` NON CI RUN`); console.info(' NODE_ENV', process.env.NODE_ENV); console.info(' NODE_APP_INSTANCE', process.env.NODE_APP_INSTANCE); diff --git a/tests/automation/test.spec.ts b/tests/automation/test.spec.ts index 167c617..61c8810 100644 --- a/tests/automation/test.spec.ts +++ b/tests/automation/test.spec.ts @@ -1,6 +1,10 @@ +import { prepareThreeFriendsInSharedGroup } from '../../state_generation'; +import { sleepFor } from '../promise_utils'; import { sessionTestOneWindow } from './setup/sessionTest'; -import { clickOnTestIdWithText } from './utilities/utils'; +import { clickOnMatchingText } from './utilities/utils'; sessionTestOneWindow('Tiny test', async ([windowA]) => { - await clickOnTestIdWithText(windowA, 'create-account-button'); + await prepareThreeFriendsInSharedGroup(); + await sleepFor(1000000); + await clickOnMatchingText(windowA, 'Create Session ID'); }); diff --git a/tests/sodium.ts b/tests/sodium.ts new file mode 100644 index 0000000..f8de133 --- /dev/null +++ b/tests/sodium.ts @@ -0,0 +1,12 @@ +import * as libSodiumWrappers from 'libsodium-wrappers-sumo'; + +export type LibSodiumType = typeof libSodiumWrappers; + +export async function getSodiumNode() { + await libSodiumWrappers.ready; + return (libSodiumWrappers as any).default as LibSodiumType; +} + +export type WithSodium = { + sodium: LibSodiumType; +}; diff --git a/yarn.lock b/yarn.lock index b3ecad2..f9b1c6e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -163,6 +163,18 @@ dependencies: "@types/node" "*" +"@types/libsodium-wrappers-sumo@^0.7.8": + version "0.7.8" + resolved "https://registry.yarnpkg.com/@types/libsodium-wrappers-sumo/-/libsodium-wrappers-sumo-0.7.8.tgz#33e32b454fb6b340758c9ffdb1f9657e1be058ff" + integrity sha512-N2+df4MB/A+W0RAcTw7A5oxKgzD+Vh6Ye7lfjWIi5SdTzVLfHPzxUjhwPqHLO5Ev9fv/+VHl+sUaUuTg4fUPqw== + dependencies: + "@types/libsodium-wrappers" "*" + +"@types/libsodium-wrappers@*": + version "0.7.14" + resolved "https://registry.yarnpkg.com/@types/libsodium-wrappers/-/libsodium-wrappers-0.7.14.tgz#f688f8d44e46ed61c401f82ff757581655fbcc42" + integrity sha512-5Kv68fXuXK0iDuUir1WPGw2R9fOZUlYlSAa0ztMcL0s0BfIDTqg9GXz8K30VJpPP3sxWhbolnQma2x+/TfkzDQ== + "@types/lodash@^4.14.196": version "4.14.196" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.196.tgz#a7c3d6fc52d8d71328b764e28e080b4169ec7a95" @@ -442,6 +454,11 @@ braces@^3.0.2: dependencies: fill-range "^7.0.1" +buffer-crc32@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-1.0.0.tgz#a10993b9055081d55304bd9feb4a072de179f405" + integrity sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w== + buffer-crc32@~0.2.3: version "0.2.13" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" @@ -1424,6 +1441,18 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" +libsodium-sumo@^0.7.14: + version "0.7.14" + resolved "https://registry.yarnpkg.com/libsodium-sumo/-/libsodium-sumo-0.7.14.tgz#9a53e09944f092f603a1e1d4446414de0b3fb0fc" + integrity sha512-2nDge6qlAjcwyslAhWfVumlkeSNK5+WCfKa2/VEq9prvlT5vP2FR0m0o5hmKaYqfsZ4TQVj5czQsimZvXDB1CQ== + +libsodium-wrappers-sumo@^0.7.14: + version "0.7.14" + resolved "https://registry.yarnpkg.com/libsodium-wrappers-sumo/-/libsodium-wrappers-sumo-0.7.14.tgz#86301f14b37a77d847eb0396f2b83cdb1c47c480" + integrity sha512-0lm7ZwN5a95J2yUi8R1rgQeeaVDIWnvNzgVmXmZswis4mC+bQtbDrB+QpJlL4qklaKx3hVpJjoc6ubzJFiv64Q== + dependencies: + libsodium-sumo "^0.7.14" + locate-path@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" @@ -1819,6 +1848,10 @@ serialize-error@^7.0.1: dependencies: type-fest "^0.13.1" +"session-tooling@git+https://github.com/Bi1b/session-tooling": + version "0.0.1" + resolved "git+https://github.com/Bi1b/session-tooling#ee3172848bf6106e6db27fedc9a24d1d963c32e6" + shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -1996,10 +2029,10 @@ typed-array-length@^1.0.4: for-each "^0.3.3" is-typed-array "^1.1.9" -typescript@^5.1.6: - version "5.1.6" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274" - integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA== +typescript@^5.5.4: + version "5.5.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba" + integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q== unbox-primitive@^1.0.2: version "1.0.2"