@parity/product-sdk-keys 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +2 -0
- package/src/key-manager.ts +2 -0
- package/src/product-account.test.ts +2 -0
- package/src/product-account.ts +2 -0
- package/src/seed-to-account.ts +2 -0
- package/src/session-key-manager.ts +2 -0
- package/src/types.ts +2 -0
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/key-manager.ts","../src/seed-to-account.ts","../src/session-key-manager.ts","../src/product-account.ts"],"names":["sr25519CreateDerive","ss58Encode","deriveH160","getPolkadotSigner"],"mappings":"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","file":"index.js","sourcesContent":["import { sr25519CreateDerive } from \"@polkadot-labs/hdkd\";\nimport { getPolkadotSigner } from \"polkadot-api/signer\";\n\nimport { deriveH160, ss58Encode } from \"@parity/product-sdk-address\";\nimport { deriveKey, nacl } from \"@parity/product-sdk-crypto\";\n\nimport type { DerivedAccount, DerivedKeypairs } from \"./types.js\";\n\nconst DEFAULT_SALT = \"product-sdk-keys-v1\";\n\nfunction hexToBytes(hex: string): Uint8Array {\n const clean = hex.startsWith(\"0x\") ? hex.slice(2) : hex;\n const bytes = new Uint8Array(clean.length / 2);\n for (let i = 0; i < bytes.length; i++) {\n bytes[i] = Number.parseInt(clean.slice(i * 2, i * 2 + 2), 16);\n }\n return bytes;\n}\n\n/**\n * Hierarchical key manager.\n *\n * Holds a 32-byte master key in memory and derives child keys via HKDF-SHA256.\n * Does not persist anything — persistence is the consumer's responsibility.\n */\nexport class KeyManager {\n private readonly masterKey: Uint8Array;\n\n private constructor(masterKey: Uint8Array) {\n this.masterKey = masterKey;\n }\n\n /**\n * Create a KeyManager from a cryptographic signature.\n *\n * Derives master key via HKDF-SHA256:\n * IKM = signatureBytes, salt = options.salt (default \"product-sdk-keys-v1\"), info = signerAddress\n *\n * @param signature - Hex string (with/without 0x prefix) or raw bytes\n * @param signerAddress - SS58 address of the signer\n * @param options.salt - HKDF salt, defaults to \"product-sdk-keys-v1\"\n */\n static fromSignature(\n signature: Uint8Array | string,\n signerAddress: string,\n options?: { salt?: string },\n ): KeyManager {\n const sigBytes =\n signature instanceof Uint8Array\n ? signature\n : hexToBytes(signature.startsWith(\"0x\") ? signature.slice(2) : signature);\n if (sigBytes.length < 32) {\n throw new Error(\n `Signature too short: expected at least 32 bytes, got ${sigBytes.length}`,\n );\n }\n const salt = options?.salt ?? DEFAULT_SALT;\n const masterKey = deriveKey(sigBytes, salt, signerAddress);\n return new KeyManager(masterKey);\n }\n\n /**\n * Create a KeyManager from raw 32-byte key material.\n * For restoring from storage, testing, etc.\n */\n static fromRawKey(masterKey: Uint8Array): KeyManager {\n if (masterKey.length !== 32) {\n throw new Error(`Expected 32-byte master key, got ${masterKey.length} bytes`);\n }\n return new KeyManager(masterKey.slice());\n }\n\n /**\n * Derive a 32-byte symmetric key for a given context string.\n *\n * Uses HKDF-SHA256: IKM=masterKey, salt=\"\", info=context\n */\n deriveSymmetricKey(context: string): Uint8Array {\n return deriveKey(this.masterKey, \"\", context);\n }\n\n /**\n * Derive a Substrate sr25519 account for a given context string.\n *\n * HKDF(masterKey, \"\", \"account:\" + context) → 32-byte seed → sr25519 keypair\n */\n deriveAccount(context: string, ss58Prefix = 42): DerivedAccount {\n const seed = deriveKey(this.masterKey, \"\", `account:${context}`);\n const derive = sr25519CreateDerive(seed);\n const keyPair = derive(\"//0\");\n\n const ss58Address = ss58Encode(keyPair.publicKey, ss58Prefix);\n const h160Address = deriveH160(keyPair.publicKey);\n const signer = getPolkadotSigner(keyPair.publicKey, \"Sr25519\", keyPair.sign);\n\n return { publicKey: keyPair.publicKey, ss58Address, h160Address, signer };\n }\n\n /**\n * Derive NaCl encryption and signing keypairs from the master key.\n *\n * - Encryption: HKDF(masterKey, \"\", \"encryption-keypair\") → nacl.box.keyPair.fromSecretKey\n * - Signing: HKDF(masterKey, \"\", \"signing-keypair\") → nacl.sign.keyPair.fromSeed\n */\n deriveKeypairs(): DerivedKeypairs {\n const encSeed = deriveKey(this.masterKey, \"\", \"encryption-keypair\");\n const encKp = nacl.box.keyPair.fromSecretKey(encSeed);\n\n const sigSeed = deriveKey(this.masterKey, \"\", \"signing-keypair\");\n const sigKp = nacl.sign.keyPair.fromSeed(sigSeed);\n\n return {\n encryption: {\n publicKey: encKp.publicKey,\n secretKey: encKp.secretKey,\n },\n signing: {\n publicKey: sigKp.publicKey,\n secretKey: sigKp.secretKey,\n },\n };\n }\n\n /**\n * Export the raw master key bytes for consumer-managed persistence.\n */\n exportKey(): Uint8Array {\n return this.masterKey.slice();\n }\n}\n\nif (import.meta.vitest) {\n const { test, expect, describe } = import.meta.vitest;\n\n const TEST_SIG = new Uint8Array(64).fill(0xaa);\n const TEST_ADDR = \"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY\";\n\n describe(\"KeyManager.fromSignature\", () => {\n test(\"deterministic master key from fixed signature\", () => {\n const a = KeyManager.fromSignature(TEST_SIG, TEST_ADDR);\n const b = KeyManager.fromSignature(TEST_SIG, TEST_ADDR);\n expect(a.exportKey()).toEqual(b.exportKey());\n expect(a.exportKey().length).toBe(32);\n });\n\n test(\"accepts hex string with 0x prefix\", () => {\n const hex = `0x${\"aa\".repeat(64)}`;\n const fromHex = KeyManager.fromSignature(hex, TEST_ADDR);\n const fromBytes = KeyManager.fromSignature(TEST_SIG, TEST_ADDR);\n expect(fromHex.exportKey()).toEqual(fromBytes.exportKey());\n });\n\n test(\"accepts hex string without 0x prefix\", () => {\n const hex = \"aa\".repeat(64);\n const fromHex = KeyManager.fromSignature(hex, TEST_ADDR);\n const fromBytes = KeyManager.fromSignature(TEST_SIG, TEST_ADDR);\n expect(fromHex.exportKey()).toEqual(fromBytes.exportKey());\n });\n\n test(\"different addresses produce different master keys\", () => {\n const a = KeyManager.fromSignature(TEST_SIG, TEST_ADDR);\n const b = KeyManager.fromSignature(\n TEST_SIG,\n \"5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty\",\n );\n expect(a.exportKey()).not.toEqual(b.exportKey());\n });\n\n test(\"custom salt produces different master key\", () => {\n const a = KeyManager.fromSignature(TEST_SIG, TEST_ADDR);\n const b = KeyManager.fromSignature(TEST_SIG, TEST_ADDR, { salt: \"custom-salt\" });\n expect(a.exportKey()).not.toEqual(b.exportKey());\n });\n\n test(\"rejects empty signature\", () => {\n expect(() => KeyManager.fromSignature(new Uint8Array(0), TEST_ADDR)).toThrow(\n \"Signature too short\",\n );\n });\n\n test(\"rejects short signature\", () => {\n expect(() => KeyManager.fromSignature(new Uint8Array(16), TEST_ADDR)).toThrow(\n \"Signature too short\",\n );\n });\n\n test(\"rejects empty hex string\", () => {\n expect(() => KeyManager.fromSignature(\"0x\", TEST_ADDR)).toThrow(\"Signature too short\");\n });\n });\n\n describe(\"KeyManager.fromRawKey\", () => {\n test(\"accepts 32-byte key\", () => {\n const key = new Uint8Array(32).fill(0xbb);\n const km = KeyManager.fromRawKey(key);\n expect(km.exportKey()).toEqual(key);\n });\n\n test(\"rejects non-32-byte input\", () => {\n expect(() => KeyManager.fromRawKey(new Uint8Array(16))).toThrow(\"Expected 32-byte\");\n expect(() => KeyManager.fromRawKey(new Uint8Array(64))).toThrow(\"Expected 32-byte\");\n });\n\n test(\"exportKey returns a copy\", () => {\n const key = new Uint8Array(32).fill(0xcc);\n const km = KeyManager.fromRawKey(key);\n const exported = km.exportKey();\n exported[0] = 0xff;\n expect(km.exportKey()[0]).toBe(0xcc);\n });\n\n test(\"copies input — mutating original does not affect internal state\", () => {\n const key = new Uint8Array(32).fill(0xaa);\n const km = KeyManager.fromRawKey(key);\n key[0] = 0xff;\n expect(km.exportKey()[0]).toBe(0xaa);\n });\n });\n\n describe(\"deriveSymmetricKey\", () => {\n test(\"deterministic for same context\", () => {\n const km = KeyManager.fromRawKey(new Uint8Array(32).fill(0xdd));\n const a = km.deriveSymmetricKey(\"doc:123\");\n const b = km.deriveSymmetricKey(\"doc:123\");\n expect(a).toEqual(b);\n expect(a.length).toBe(32);\n });\n\n test(\"different contexts produce different keys\", () => {\n const km = KeyManager.fromRawKey(new Uint8Array(32).fill(0xdd));\n const a = km.deriveSymmetricKey(\"doc:123\");\n const b = km.deriveSymmetricKey(\"doc:456\");\n expect(a).not.toEqual(b);\n });\n\n test(\"empty context string works\", () => {\n const km = KeyManager.fromRawKey(new Uint8Array(32).fill(0xdd));\n const key = km.deriveSymmetricKey(\"\");\n expect(key.length).toBe(32);\n });\n\n test(\"deriveSymmetricKey and deriveAccount use different domains\", () => {\n const km = KeyManager.fromRawKey(new Uint8Array(32).fill(0xdd));\n const symKey = km.deriveSymmetricKey(\"foo\");\n const accountSeed = km.deriveSymmetricKey(\"account:foo\");\n // deriveAccount(\"foo\") uses info=\"account:foo\" internally,\n // so its HKDF output matches deriveSymmetricKey(\"account:foo\")\n // but the final account is further derived through sr25519\n expect(symKey).not.toEqual(accountSeed);\n });\n });\n\n describe(\"deriveAccount\", () => {\n test(\"deterministic for same context\", () => {\n const km = KeyManager.fromRawKey(new Uint8Array(32).fill(0xee));\n const a = km.deriveAccount(\"doc-account:123\");\n const b = km.deriveAccount(\"doc-account:123\");\n expect(a.ss58Address).toBe(b.ss58Address);\n expect(a.h160Address).toBe(b.h160Address);\n expect(a.publicKey).toEqual(b.publicKey);\n });\n\n test(\"produces valid addresses\", () => {\n const km = KeyManager.fromRawKey(new Uint8Array(32).fill(0xee));\n const account = km.deriveAccount(\"test\");\n expect(account.ss58Address).toMatch(/^[1-9A-HJ-NP-Za-km-z]+$/);\n expect(account.h160Address).toMatch(/^0x[a-f0-9]{40}$/);\n expect(account.publicKey.length).toBe(32);\n });\n\n test(\"different contexts produce different accounts\", () => {\n const km = KeyManager.fromRawKey(new Uint8Array(32).fill(0xee));\n const a = km.deriveAccount(\"ctx-a\");\n const b = km.deriveAccount(\"ctx-b\");\n expect(a.ss58Address).not.toBe(b.ss58Address);\n });\n\n test(\"custom ss58Prefix changes address encoding\", () => {\n const km = KeyManager.fromRawKey(new Uint8Array(32).fill(0xee));\n const generic = km.deriveAccount(\"test\", 42);\n const polkadot = km.deriveAccount(\"test\", 0);\n expect(generic.ss58Address).not.toBe(polkadot.ss58Address);\n expect(generic.publicKey).toEqual(polkadot.publicKey);\n });\n\n test(\"signer has correct publicKey\", () => {\n const km = KeyManager.fromRawKey(new Uint8Array(32).fill(0xee));\n const account = km.deriveAccount(\"test\");\n expect(account.signer.publicKey).toEqual(account.publicKey);\n });\n });\n\n describe(\"deriveKeypairs\", () => {\n test(\"deterministic from same master key\", () => {\n const km = KeyManager.fromRawKey(new Uint8Array(32).fill(0xff));\n const a = km.deriveKeypairs();\n const b = km.deriveKeypairs();\n expect(a.encryption.publicKey).toEqual(b.encryption.publicKey);\n expect(a.signing.publicKey).toEqual(b.signing.publicKey);\n });\n\n test(\"NaCl Box encrypt/decrypt round-trip\", () => {\n const km = KeyManager.fromRawKey(new Uint8Array(32).fill(0xff));\n const kp = km.deriveKeypairs();\n const message = new TextEncoder().encode(\"hello keys\");\n const nonce = nacl.randomBytes(24);\n const encrypted = nacl.box(\n message,\n nonce,\n kp.encryption.publicKey,\n kp.encryption.secretKey,\n );\n expect(encrypted).not.toBeNull();\n const decrypted = nacl.box.open(\n encrypted!,\n nonce,\n kp.encryption.publicKey,\n kp.encryption.secretKey,\n );\n expect(new TextDecoder().decode(decrypted!)).toBe(\"hello keys\");\n });\n\n test(\"NaCl Box two-party encrypt/decrypt\", () => {\n const kmA = KeyManager.fromRawKey(new Uint8Array(32).fill(0xaa));\n const kmB = KeyManager.fromRawKey(new Uint8Array(32).fill(0xbb));\n const kpA = kmA.deriveKeypairs();\n const kpB = kmB.deriveKeypairs();\n const message = new TextEncoder().encode(\"secret for B\");\n const nonce = nacl.randomBytes(24);\n // A encrypts for B\n const encrypted = nacl.box(\n message,\n nonce,\n kpB.encryption.publicKey,\n kpA.encryption.secretKey,\n );\n expect(encrypted).not.toBeNull();\n // B decrypts from A\n const decrypted = nacl.box.open(\n encrypted!,\n nonce,\n kpA.encryption.publicKey,\n kpB.encryption.secretKey,\n );\n expect(new TextDecoder().decode(decrypted!)).toBe(\"secret for B\");\n });\n\n test(\"NaCl Sign sign/verify round-trip\", () => {\n const km = KeyManager.fromRawKey(new Uint8Array(32).fill(0xff));\n const kp = km.deriveKeypairs();\n const message = new TextEncoder().encode(\"sign this\");\n const signed = nacl.sign(message, kp.signing.secretKey);\n const opened = nacl.sign.open(signed, kp.signing.publicKey);\n expect(new TextDecoder().decode(opened!)).toBe(\"sign this\");\n });\n });\n}\n","import { ed25519CreateDerive, sr25519CreateDerive } from \"@polkadot-labs/hdkd\";\nimport { entropyToMiniSecret, mnemonicToEntropy } from \"@polkadot-labs/hdkd-helpers\";\nimport { getPolkadotSigner } from \"polkadot-api/signer\";\n\nimport { deriveH160, ss58Encode } from \"@parity/product-sdk-address\";\n\nimport type { DerivedAccount } from \"./types.js\";\n\n/**\n * Derive a DerivedAccount from a BIP39 mnemonic phrase.\n *\n * Uses the specified key type for derivation with a hard derivation path\n * (default `\"//0\"`).\n *\n * @param mnemonic - BIP39 mnemonic phrase\n * @param derivationPath - Hard derivation path, defaults to `\"//0\"`\n * @param ss58Prefix - SS58 network prefix, defaults to 42 (generic)\n * @param keyType - Key type for derivation, either `\"sr25519\"` or `\"ed25519\"`, defaults to `\"sr25519\"`\n */\nexport function seedToAccount(\n mnemonic: string,\n derivationPath = \"//0\",\n ss58Prefix = 42,\n keyType: \"sr25519\" | \"ed25519\" = \"sr25519\",\n): DerivedAccount {\n let entropy: Uint8Array;\n try {\n entropy = mnemonicToEntropy(mnemonic);\n } catch (cause) {\n throw new Error(\"Invalid mnemonic phrase\", { cause });\n }\n const miniSecret = entropyToMiniSecret(entropy);\n const derive =\n keyType === \"ed25519\" ? ed25519CreateDerive(miniSecret) : sr25519CreateDerive(miniSecret);\n const keyPair = derive(derivationPath);\n\n const signerKeyType = keyType === \"ed25519\" ? \"Ed25519\" : \"Sr25519\";\n const ss58Address = ss58Encode(keyPair.publicKey, ss58Prefix);\n const h160Address = deriveH160(keyPair.publicKey);\n const signer = getPolkadotSigner(keyPair.publicKey, signerKeyType, keyPair.sign);\n\n return {\n publicKey: keyPair.publicKey,\n ss58Address,\n h160Address,\n signer,\n };\n}\n\nif (import.meta.vitest) {\n const { test, expect, describe } = import.meta.vitest;\n const { generateMnemonic } = await import(\"@polkadot-labs/hdkd-helpers\");\n\n // Fixed test mnemonic (DO NOT use in production)\n const TEST_MNEMONIC =\n \"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about\";\n\n describe(\"seedToAccount\", () => {\n test(\"deterministic derivation from fixed mnemonic\", () => {\n const a = seedToAccount(TEST_MNEMONIC);\n const b = seedToAccount(TEST_MNEMONIC);\n expect(a.ss58Address).toBe(b.ss58Address);\n expect(a.h160Address).toBe(b.h160Address);\n expect(a.publicKey).toEqual(b.publicKey);\n expect(a.publicKey.length).toBe(32);\n });\n\n test(\"returns valid SS58 and H160 addresses\", () => {\n const account = seedToAccount(TEST_MNEMONIC);\n expect(account.ss58Address).toMatch(/^[1-9A-HJ-NP-Za-km-z]+$/);\n expect(account.h160Address).toMatch(/^0x[a-f0-9]{40}$/);\n });\n\n test(\"custom derivation path produces different addresses\", () => {\n const a = seedToAccount(TEST_MNEMONIC, \"//0\");\n const b = seedToAccount(TEST_MNEMONIC, \"//1\");\n expect(a.ss58Address).not.toBe(b.ss58Address);\n expect(a.h160Address).not.toBe(b.h160Address);\n });\n\n test(\"custom SS58 prefix changes address encoding\", () => {\n const generic = seedToAccount(TEST_MNEMONIC, \"//0\", 42);\n const polkadot = seedToAccount(TEST_MNEMONIC, \"//0\", 0);\n expect(generic.ss58Address).not.toBe(polkadot.ss58Address);\n // Same underlying public key\n expect(generic.publicKey).toEqual(polkadot.publicKey);\n expect(generic.h160Address).toBe(polkadot.h160Address);\n });\n\n test(\"provides a signer\", () => {\n const account = seedToAccount(TEST_MNEMONIC);\n expect(account.signer).toBeDefined();\n expect(account.signer.publicKey).toEqual(account.publicKey);\n });\n\n test(\"works with a freshly generated mnemonic\", () => {\n const mnemonic = generateMnemonic();\n const account = seedToAccount(mnemonic);\n expect(account.ss58Address).toBeTruthy();\n expect(account.publicKey.length).toBe(32);\n });\n\n test(\"throws descriptive error for invalid mnemonic\", () => {\n expect(() => seedToAccount(\"not a valid mnemonic\")).toThrow(\"Invalid mnemonic phrase\");\n });\n\n test(\"throws descriptive error for empty string\", () => {\n expect(() => seedToAccount(\"\")).toThrow(\"Invalid mnemonic phrase\");\n });\n\n test(\"ed25519 derivation produces different address than sr25519\", () => {\n const sr = seedToAccount(TEST_MNEMONIC, \"//0\", 42, \"sr25519\");\n const ed = seedToAccount(TEST_MNEMONIC, \"//0\", 42, \"ed25519\");\n expect(ed.ss58Address).not.toBe(sr.ss58Address);\n expect(ed.publicKey).not.toEqual(sr.publicKey);\n });\n\n test(\"ed25519 derivation is deterministic\", () => {\n const a = seedToAccount(TEST_MNEMONIC, \"//0\", 42, \"ed25519\");\n const b = seedToAccount(TEST_MNEMONIC, \"//0\", 42, \"ed25519\");\n expect(a.ss58Address).toBe(b.ss58Address);\n expect(a.h160Address).toBe(b.h160Address);\n expect(a.publicKey).toEqual(b.publicKey);\n expect(a.publicKey.length).toBe(32);\n });\n\n test(\"ed25519 provides a signer with matching publicKey\", () => {\n const account = seedToAccount(TEST_MNEMONIC, \"//0\", 42, \"ed25519\");\n expect(account.signer).toBeDefined();\n expect(account.signer.publicKey).toEqual(account.publicKey);\n });\n\n test(\"default keyType is sr25519 (backward compatible)\", () => {\n const withDefault = seedToAccount(TEST_MNEMONIC);\n const withExplicit = seedToAccount(TEST_MNEMONIC, \"//0\", 42, \"sr25519\");\n expect(withDefault.ss58Address).toBe(withExplicit.ss58Address);\n expect(withDefault.publicKey).toEqual(withExplicit.publicKey);\n });\n });\n}\n","import { generateMnemonic } from \"@polkadot-labs/hdkd-helpers\";\nimport type { LocalKvStore } from \"@parity/product-sdk-local-storage\";\n\nimport { seedToAccount } from \"./seed-to-account.js\";\nimport type { SessionKeyInfo } from \"./types.js\";\n\n/**\n * Manages an sr25519 account derived from a BIP39 mnemonic.\n *\n * @param options.store - LocalKvStore instance (from `@parity/product-sdk-local-storage`).\n * Create with `createLocalKvStore({ prefix: \"session-key\" })` for namespaced persistence.\n * @param options.name - Identifies this session key. Defaults to `\"default\"`.\n * Use different names to manage multiple independent session keys.\n *\n * @example\n * ```ts\n * const store = await createLocalKvStore({ prefix: \"session-key\" });\n * const skm = new SessionKeyManager({ store });\n * const key = await skm.getOrCreate();\n * ```\n */\nexport class SessionKeyManager {\n private readonly name: string;\n private readonly store: LocalKvStore;\n\n constructor(options: { store: LocalKvStore; name?: string }) {\n this.name = options.name ?? \"default\";\n this.store = options.store;\n }\n\n /**\n * Create a new session key from a fresh mnemonic.\n * Persists the mnemonic to the store.\n */\n async create(): Promise<SessionKeyInfo> {\n const mnemonic = generateMnemonic();\n await this.store.set(this.name, mnemonic);\n return { mnemonic, account: seedToAccount(mnemonic) };\n }\n\n /**\n * Load an existing session key from the store.\n * Returns null if no mnemonic is stored.\n */\n async get(): Promise<SessionKeyInfo | null> {\n const mnemonic = await this.store.get(this.name);\n if (!mnemonic) return null;\n return { mnemonic, account: seedToAccount(mnemonic) };\n }\n\n /**\n * Load existing or create a new session key.\n */\n async getOrCreate(): Promise<SessionKeyInfo> {\n const existing = await this.get();\n if (existing) return existing;\n return this.create();\n }\n\n /**\n * Derive a session key from an explicit mnemonic (no storage interaction).\n */\n fromMnemonic(mnemonic: string): SessionKeyInfo {\n return { mnemonic, account: seedToAccount(mnemonic) };\n }\n\n /**\n * Clear the stored mnemonic from the store.\n */\n async clear(): Promise<void> {\n await this.store.remove(this.name);\n }\n}\n\nif (import.meta.vitest) {\n const { test, expect, describe } = import.meta.vitest;\n\n const TEST_MNEMONIC =\n \"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about\";\n\n function mockLocalKvStore(): LocalKvStore & { data: Map<string, string> } {\n const data = new Map<string, string>();\n return {\n data,\n async get(key) {\n return data.get(key) ?? null;\n },\n async set(key, value) {\n data.set(key, value);\n },\n async remove(key) {\n data.delete(key);\n },\n async getJSON() {\n return null;\n },\n async setJSON() {},\n };\n }\n\n describe(\"SessionKeyManager\", () => {\n test(\"fromMnemonic produces deterministic results\", () => {\n const store = mockLocalKvStore();\n const skm = new SessionKeyManager({ store });\n const a = skm.fromMnemonic(TEST_MNEMONIC);\n const b = skm.fromMnemonic(TEST_MNEMONIC);\n expect(a.mnemonic).toBe(TEST_MNEMONIC);\n expect(a.account.ss58Address).toBe(b.account.ss58Address);\n expect(a.account.h160Address).toBe(b.account.h160Address);\n });\n\n test(\"fromMnemonic throws on invalid mnemonic\", () => {\n const store = mockLocalKvStore();\n const skm = new SessionKeyManager({ store });\n expect(() => skm.fromMnemonic(\"invalid words here\")).toThrow(\"Invalid mnemonic phrase\");\n });\n\n test(\"create and get round-trip\", async () => {\n const store = mockLocalKvStore();\n const skm = new SessionKeyManager({ store });\n const info = await skm.create();\n expect(info.mnemonic).toBeTruthy();\n expect(info.account.ss58Address).toMatch(/^[1-9A-HJ-NP-Za-km-z]+$/);\n expect(store.data.get(\"default\")).toBe(info.mnemonic);\n\n const loaded = await skm.get();\n expect(loaded?.mnemonic).toBe(info.mnemonic);\n });\n\n test(\"get returns null when no key stored\", async () => {\n const store = mockLocalKvStore();\n const skm = new SessionKeyManager({ store });\n expect(await skm.get()).toBeNull();\n });\n\n test(\"getOrCreate creates then returns cached\", async () => {\n const store = mockLocalKvStore();\n const skm = new SessionKeyManager({ store });\n const created = await skm.getOrCreate();\n expect(store.data.size).toBe(1);\n\n const loaded = await skm.getOrCreate();\n expect(loaded.mnemonic).toBe(created.mnemonic);\n expect(loaded.account.ss58Address).toBe(created.account.ss58Address);\n });\n\n test(\"clear removes mnemonic from store\", async () => {\n const store = mockLocalKvStore();\n const skm = new SessionKeyManager({ store });\n await skm.create();\n expect(store.data.size).toBe(1);\n\n await skm.clear();\n expect(store.data.size).toBe(0);\n expect(await skm.get()).toBeNull();\n });\n\n test(\"name separates storage keys\", async () => {\n const store = mockLocalKvStore();\n const main = new SessionKeyManager({ name: \"main\", store });\n const burner = new SessionKeyManager({ name: \"burner\", store });\n\n const mainInfo = await main.create();\n const burnerInfo = await burner.create();\n\n expect(store.data.get(\"main\")).toBe(mainInfo.mnemonic);\n expect(store.data.get(\"burner\")).toBe(burnerInfo.mnemonic);\n expect(mainInfo.account.ss58Address).not.toBe(burnerInfo.account.ss58Address);\n\n await main.clear();\n expect(store.data.has(\"main\")).toBe(false);\n expect(store.data.get(\"burner\")).toBe(burnerInfo.mnemonic);\n });\n });\n}\n","/**\n * Canonical sr25519 product-account public-key derivation.\n *\n * Mirrored byte-for-byte by polkadot-desktop\n * (`polkadot-desktop/src/domains/product/account/service.ts`) and conceptually\n * by polkadot-app-android-v2\n * (`feature/products/impl/.../ProductAccountDerivationUseCase.kt`).\n *\n * The function works on the parent *public* key alone: sr25519 soft derivation\n * is composable on public keys, so the CLI / web host / any external client can\n * compute the same derived address that the mobile wallet derives privately,\n * without ever seeing the secret key.\n *\n * Junction path: [\"product\", productId, String(derivationIndex)], applied\n * left-to-right. For each junction, a 32-byte chain code is built:\n * - numeric (\"^\\d+$\") -> SCALE u64 (BigInt), zero-padded to 32 bytes\n * - string -> SCALE str (compact-length + UTF-8), zero-padded\n * - if encoded > 32 bytes -> blake2b256(encoded) (32-byte BLAKE2b digest)\n *\n * # productId constraint (cross-platform parity)\n *\n * `productId` MUST contain at least one non-hex character or be of odd\n * length when serialized as a string. polkadot-app-android-v2's\n * SubstrateJunctionDecoder tries to interpret a junction as hex BEFORE\n * falling through to SCALE-string encoding; polkadot-desktop and this\n * implementation skip that hex branch. For productIds that happen to be\n * even-length all-hex strings (e.g. \"deadbeef\", \"c0ffee01\"), Android would\n * derive a different public key than desktop or this implementation. In\n * practice, productIds are always dotNS names (e.g. \"playground.dot\"),\n * which contain \".\" and therefore never trip the hex branch on Android.\n */\n\nimport { blake2b256 } from \"@parity/product-sdk-crypto\";\nimport { HDKD } from \"@scure/sr25519\";\nimport { str, u64 } from \"scale-ts\";\n\nconst JUNCTION_ID_LEN = 32;\nconst NON_NEGATIVE_INTEGER = /^\\d+$/;\n\nexport function createChainCode(code: string): Uint8Array {\n const encoded = NON_NEGATIVE_INTEGER.test(code) ? u64.enc(BigInt(code)) : str.enc(code);\n\n if (encoded.length > JUNCTION_ID_LEN) {\n return blake2b256(encoded);\n }\n\n const chainCode = new Uint8Array(JUNCTION_ID_LEN);\n chainCode.set(encoded);\n return chainCode;\n}\n\nexport function deriveProductAccountPublicKey(\n parentPublicKey: Uint8Array,\n productId: string,\n derivationIndex: number,\n): Uint8Array {\n const junctions = [\"product\", productId, String(derivationIndex)];\n return junctions.reduce<Uint8Array>(\n (pubkey, junction) => HDKD.publicSoft(pubkey, createChainCode(junction)),\n parentPublicKey,\n );\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/key-manager.ts","../src/seed-to-account.ts","../src/session-key-manager.ts","../src/product-account.ts"],"names":["sr25519CreateDerive","ss58Encode","deriveH160","getPolkadotSigner"],"mappings":"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","file":"index.js","sourcesContent":["// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\nimport { sr25519CreateDerive } from \"@polkadot-labs/hdkd\";\nimport { getPolkadotSigner } from \"polkadot-api/signer\";\n\nimport { deriveH160, ss58Encode } from \"@parity/product-sdk-address\";\nimport { deriveKey, nacl } from \"@parity/product-sdk-crypto\";\n\nimport type { DerivedAccount, DerivedKeypairs } from \"./types.js\";\n\nconst DEFAULT_SALT = \"product-sdk-keys-v1\";\n\nfunction hexToBytes(hex: string): Uint8Array {\n const clean = hex.startsWith(\"0x\") ? hex.slice(2) : hex;\n const bytes = new Uint8Array(clean.length / 2);\n for (let i = 0; i < bytes.length; i++) {\n bytes[i] = Number.parseInt(clean.slice(i * 2, i * 2 + 2), 16);\n }\n return bytes;\n}\n\n/**\n * Hierarchical key manager.\n *\n * Holds a 32-byte master key in memory and derives child keys via HKDF-SHA256.\n * Does not persist anything — persistence is the consumer's responsibility.\n */\nexport class KeyManager {\n private readonly masterKey: Uint8Array;\n\n private constructor(masterKey: Uint8Array) {\n this.masterKey = masterKey;\n }\n\n /**\n * Create a KeyManager from a cryptographic signature.\n *\n * Derives master key via HKDF-SHA256:\n * IKM = signatureBytes, salt = options.salt (default \"product-sdk-keys-v1\"), info = signerAddress\n *\n * @param signature - Hex string (with/without 0x prefix) or raw bytes\n * @param signerAddress - SS58 address of the signer\n * @param options.salt - HKDF salt, defaults to \"product-sdk-keys-v1\"\n */\n static fromSignature(\n signature: Uint8Array | string,\n signerAddress: string,\n options?: { salt?: string },\n ): KeyManager {\n const sigBytes =\n signature instanceof Uint8Array\n ? signature\n : hexToBytes(signature.startsWith(\"0x\") ? signature.slice(2) : signature);\n if (sigBytes.length < 32) {\n throw new Error(\n `Signature too short: expected at least 32 bytes, got ${sigBytes.length}`,\n );\n }\n const salt = options?.salt ?? DEFAULT_SALT;\n const masterKey = deriveKey(sigBytes, salt, signerAddress);\n return new KeyManager(masterKey);\n }\n\n /**\n * Create a KeyManager from raw 32-byte key material.\n * For restoring from storage, testing, etc.\n */\n static fromRawKey(masterKey: Uint8Array): KeyManager {\n if (masterKey.length !== 32) {\n throw new Error(`Expected 32-byte master key, got ${masterKey.length} bytes`);\n }\n return new KeyManager(masterKey.slice());\n }\n\n /**\n * Derive a 32-byte symmetric key for a given context string.\n *\n * Uses HKDF-SHA256: IKM=masterKey, salt=\"\", info=context\n */\n deriveSymmetricKey(context: string): Uint8Array {\n return deriveKey(this.masterKey, \"\", context);\n }\n\n /**\n * Derive a Substrate sr25519 account for a given context string.\n *\n * HKDF(masterKey, \"\", \"account:\" + context) → 32-byte seed → sr25519 keypair\n */\n deriveAccount(context: string, ss58Prefix = 42): DerivedAccount {\n const seed = deriveKey(this.masterKey, \"\", `account:${context}`);\n const derive = sr25519CreateDerive(seed);\n const keyPair = derive(\"//0\");\n\n const ss58Address = ss58Encode(keyPair.publicKey, ss58Prefix);\n const h160Address = deriveH160(keyPair.publicKey);\n const signer = getPolkadotSigner(keyPair.publicKey, \"Sr25519\", keyPair.sign);\n\n return { publicKey: keyPair.publicKey, ss58Address, h160Address, signer };\n }\n\n /**\n * Derive NaCl encryption and signing keypairs from the master key.\n *\n * - Encryption: HKDF(masterKey, \"\", \"encryption-keypair\") → nacl.box.keyPair.fromSecretKey\n * - Signing: HKDF(masterKey, \"\", \"signing-keypair\") → nacl.sign.keyPair.fromSeed\n */\n deriveKeypairs(): DerivedKeypairs {\n const encSeed = deriveKey(this.masterKey, \"\", \"encryption-keypair\");\n const encKp = nacl.box.keyPair.fromSecretKey(encSeed);\n\n const sigSeed = deriveKey(this.masterKey, \"\", \"signing-keypair\");\n const sigKp = nacl.sign.keyPair.fromSeed(sigSeed);\n\n return {\n encryption: {\n publicKey: encKp.publicKey,\n secretKey: encKp.secretKey,\n },\n signing: {\n publicKey: sigKp.publicKey,\n secretKey: sigKp.secretKey,\n },\n };\n }\n\n /**\n * Export the raw master key bytes for consumer-managed persistence.\n */\n exportKey(): Uint8Array {\n return this.masterKey.slice();\n }\n}\n\nif (import.meta.vitest) {\n const { test, expect, describe } = import.meta.vitest;\n\n const TEST_SIG = new Uint8Array(64).fill(0xaa);\n const TEST_ADDR = \"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY\";\n\n describe(\"KeyManager.fromSignature\", () => {\n test(\"deterministic master key from fixed signature\", () => {\n const a = KeyManager.fromSignature(TEST_SIG, TEST_ADDR);\n const b = KeyManager.fromSignature(TEST_SIG, TEST_ADDR);\n expect(a.exportKey()).toEqual(b.exportKey());\n expect(a.exportKey().length).toBe(32);\n });\n\n test(\"accepts hex string with 0x prefix\", () => {\n const hex = `0x${\"aa\".repeat(64)}`;\n const fromHex = KeyManager.fromSignature(hex, TEST_ADDR);\n const fromBytes = KeyManager.fromSignature(TEST_SIG, TEST_ADDR);\n expect(fromHex.exportKey()).toEqual(fromBytes.exportKey());\n });\n\n test(\"accepts hex string without 0x prefix\", () => {\n const hex = \"aa\".repeat(64);\n const fromHex = KeyManager.fromSignature(hex, TEST_ADDR);\n const fromBytes = KeyManager.fromSignature(TEST_SIG, TEST_ADDR);\n expect(fromHex.exportKey()).toEqual(fromBytes.exportKey());\n });\n\n test(\"different addresses produce different master keys\", () => {\n const a = KeyManager.fromSignature(TEST_SIG, TEST_ADDR);\n const b = KeyManager.fromSignature(\n TEST_SIG,\n \"5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty\",\n );\n expect(a.exportKey()).not.toEqual(b.exportKey());\n });\n\n test(\"custom salt produces different master key\", () => {\n const a = KeyManager.fromSignature(TEST_SIG, TEST_ADDR);\n const b = KeyManager.fromSignature(TEST_SIG, TEST_ADDR, { salt: \"custom-salt\" });\n expect(a.exportKey()).not.toEqual(b.exportKey());\n });\n\n test(\"rejects empty signature\", () => {\n expect(() => KeyManager.fromSignature(new Uint8Array(0), TEST_ADDR)).toThrow(\n \"Signature too short\",\n );\n });\n\n test(\"rejects short signature\", () => {\n expect(() => KeyManager.fromSignature(new Uint8Array(16), TEST_ADDR)).toThrow(\n \"Signature too short\",\n );\n });\n\n test(\"rejects empty hex string\", () => {\n expect(() => KeyManager.fromSignature(\"0x\", TEST_ADDR)).toThrow(\"Signature too short\");\n });\n });\n\n describe(\"KeyManager.fromRawKey\", () => {\n test(\"accepts 32-byte key\", () => {\n const key = new Uint8Array(32).fill(0xbb);\n const km = KeyManager.fromRawKey(key);\n expect(km.exportKey()).toEqual(key);\n });\n\n test(\"rejects non-32-byte input\", () => {\n expect(() => KeyManager.fromRawKey(new Uint8Array(16))).toThrow(\"Expected 32-byte\");\n expect(() => KeyManager.fromRawKey(new Uint8Array(64))).toThrow(\"Expected 32-byte\");\n });\n\n test(\"exportKey returns a copy\", () => {\n const key = new Uint8Array(32).fill(0xcc);\n const km = KeyManager.fromRawKey(key);\n const exported = km.exportKey();\n exported[0] = 0xff;\n expect(km.exportKey()[0]).toBe(0xcc);\n });\n\n test(\"copies input — mutating original does not affect internal state\", () => {\n const key = new Uint8Array(32).fill(0xaa);\n const km = KeyManager.fromRawKey(key);\n key[0] = 0xff;\n expect(km.exportKey()[0]).toBe(0xaa);\n });\n });\n\n describe(\"deriveSymmetricKey\", () => {\n test(\"deterministic for same context\", () => {\n const km = KeyManager.fromRawKey(new Uint8Array(32).fill(0xdd));\n const a = km.deriveSymmetricKey(\"doc:123\");\n const b = km.deriveSymmetricKey(\"doc:123\");\n expect(a).toEqual(b);\n expect(a.length).toBe(32);\n });\n\n test(\"different contexts produce different keys\", () => {\n const km = KeyManager.fromRawKey(new Uint8Array(32).fill(0xdd));\n const a = km.deriveSymmetricKey(\"doc:123\");\n const b = km.deriveSymmetricKey(\"doc:456\");\n expect(a).not.toEqual(b);\n });\n\n test(\"empty context string works\", () => {\n const km = KeyManager.fromRawKey(new Uint8Array(32).fill(0xdd));\n const key = km.deriveSymmetricKey(\"\");\n expect(key.length).toBe(32);\n });\n\n test(\"deriveSymmetricKey and deriveAccount use different domains\", () => {\n const km = KeyManager.fromRawKey(new Uint8Array(32).fill(0xdd));\n const symKey = km.deriveSymmetricKey(\"foo\");\n const accountSeed = km.deriveSymmetricKey(\"account:foo\");\n // deriveAccount(\"foo\") uses info=\"account:foo\" internally,\n // so its HKDF output matches deriveSymmetricKey(\"account:foo\")\n // but the final account is further derived through sr25519\n expect(symKey).not.toEqual(accountSeed);\n });\n });\n\n describe(\"deriveAccount\", () => {\n test(\"deterministic for same context\", () => {\n const km = KeyManager.fromRawKey(new Uint8Array(32).fill(0xee));\n const a = km.deriveAccount(\"doc-account:123\");\n const b = km.deriveAccount(\"doc-account:123\");\n expect(a.ss58Address).toBe(b.ss58Address);\n expect(a.h160Address).toBe(b.h160Address);\n expect(a.publicKey).toEqual(b.publicKey);\n });\n\n test(\"produces valid addresses\", () => {\n const km = KeyManager.fromRawKey(new Uint8Array(32).fill(0xee));\n const account = km.deriveAccount(\"test\");\n expect(account.ss58Address).toMatch(/^[1-9A-HJ-NP-Za-km-z]+$/);\n expect(account.h160Address).toMatch(/^0x[a-f0-9]{40}$/);\n expect(account.publicKey.length).toBe(32);\n });\n\n test(\"different contexts produce different accounts\", () => {\n const km = KeyManager.fromRawKey(new Uint8Array(32).fill(0xee));\n const a = km.deriveAccount(\"ctx-a\");\n const b = km.deriveAccount(\"ctx-b\");\n expect(a.ss58Address).not.toBe(b.ss58Address);\n });\n\n test(\"custom ss58Prefix changes address encoding\", () => {\n const km = KeyManager.fromRawKey(new Uint8Array(32).fill(0xee));\n const generic = km.deriveAccount(\"test\", 42);\n const polkadot = km.deriveAccount(\"test\", 0);\n expect(generic.ss58Address).not.toBe(polkadot.ss58Address);\n expect(generic.publicKey).toEqual(polkadot.publicKey);\n });\n\n test(\"signer has correct publicKey\", () => {\n const km = KeyManager.fromRawKey(new Uint8Array(32).fill(0xee));\n const account = km.deriveAccount(\"test\");\n expect(account.signer.publicKey).toEqual(account.publicKey);\n });\n });\n\n describe(\"deriveKeypairs\", () => {\n test(\"deterministic from same master key\", () => {\n const km = KeyManager.fromRawKey(new Uint8Array(32).fill(0xff));\n const a = km.deriveKeypairs();\n const b = km.deriveKeypairs();\n expect(a.encryption.publicKey).toEqual(b.encryption.publicKey);\n expect(a.signing.publicKey).toEqual(b.signing.publicKey);\n });\n\n test(\"NaCl Box encrypt/decrypt round-trip\", () => {\n const km = KeyManager.fromRawKey(new Uint8Array(32).fill(0xff));\n const kp = km.deriveKeypairs();\n const message = new TextEncoder().encode(\"hello keys\");\n const nonce = nacl.randomBytes(24);\n const encrypted = nacl.box(\n message,\n nonce,\n kp.encryption.publicKey,\n kp.encryption.secretKey,\n );\n expect(encrypted).not.toBeNull();\n const decrypted = nacl.box.open(\n encrypted!,\n nonce,\n kp.encryption.publicKey,\n kp.encryption.secretKey,\n );\n expect(new TextDecoder().decode(decrypted!)).toBe(\"hello keys\");\n });\n\n test(\"NaCl Box two-party encrypt/decrypt\", () => {\n const kmA = KeyManager.fromRawKey(new Uint8Array(32).fill(0xaa));\n const kmB = KeyManager.fromRawKey(new Uint8Array(32).fill(0xbb));\n const kpA = kmA.deriveKeypairs();\n const kpB = kmB.deriveKeypairs();\n const message = new TextEncoder().encode(\"secret for B\");\n const nonce = nacl.randomBytes(24);\n // A encrypts for B\n const encrypted = nacl.box(\n message,\n nonce,\n kpB.encryption.publicKey,\n kpA.encryption.secretKey,\n );\n expect(encrypted).not.toBeNull();\n // B decrypts from A\n const decrypted = nacl.box.open(\n encrypted!,\n nonce,\n kpA.encryption.publicKey,\n kpB.encryption.secretKey,\n );\n expect(new TextDecoder().decode(decrypted!)).toBe(\"secret for B\");\n });\n\n test(\"NaCl Sign sign/verify round-trip\", () => {\n const km = KeyManager.fromRawKey(new Uint8Array(32).fill(0xff));\n const kp = km.deriveKeypairs();\n const message = new TextEncoder().encode(\"sign this\");\n const signed = nacl.sign(message, kp.signing.secretKey);\n const opened = nacl.sign.open(signed, kp.signing.publicKey);\n expect(new TextDecoder().decode(opened!)).toBe(\"sign this\");\n });\n });\n}\n","// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\nimport { ed25519CreateDerive, sr25519CreateDerive } from \"@polkadot-labs/hdkd\";\nimport { entropyToMiniSecret, mnemonicToEntropy } from \"@polkadot-labs/hdkd-helpers\";\nimport { getPolkadotSigner } from \"polkadot-api/signer\";\n\nimport { deriveH160, ss58Encode } from \"@parity/product-sdk-address\";\n\nimport type { DerivedAccount } from \"./types.js\";\n\n/**\n * Derive a DerivedAccount from a BIP39 mnemonic phrase.\n *\n * Uses the specified key type for derivation with a hard derivation path\n * (default `\"//0\"`).\n *\n * @param mnemonic - BIP39 mnemonic phrase\n * @param derivationPath - Hard derivation path, defaults to `\"//0\"`\n * @param ss58Prefix - SS58 network prefix, defaults to 42 (generic)\n * @param keyType - Key type for derivation, either `\"sr25519\"` or `\"ed25519\"`, defaults to `\"sr25519\"`\n */\nexport function seedToAccount(\n mnemonic: string,\n derivationPath = \"//0\",\n ss58Prefix = 42,\n keyType: \"sr25519\" | \"ed25519\" = \"sr25519\",\n): DerivedAccount {\n let entropy: Uint8Array;\n try {\n entropy = mnemonicToEntropy(mnemonic);\n } catch (cause) {\n throw new Error(\"Invalid mnemonic phrase\", { cause });\n }\n const miniSecret = entropyToMiniSecret(entropy);\n const derive =\n keyType === \"ed25519\" ? ed25519CreateDerive(miniSecret) : sr25519CreateDerive(miniSecret);\n const keyPair = derive(derivationPath);\n\n const signerKeyType = keyType === \"ed25519\" ? \"Ed25519\" : \"Sr25519\";\n const ss58Address = ss58Encode(keyPair.publicKey, ss58Prefix);\n const h160Address = deriveH160(keyPair.publicKey);\n const signer = getPolkadotSigner(keyPair.publicKey, signerKeyType, keyPair.sign);\n\n return {\n publicKey: keyPair.publicKey,\n ss58Address,\n h160Address,\n signer,\n };\n}\n\nif (import.meta.vitest) {\n const { test, expect, describe } = import.meta.vitest;\n const { generateMnemonic } = await import(\"@polkadot-labs/hdkd-helpers\");\n\n // Fixed test mnemonic (DO NOT use in production)\n const TEST_MNEMONIC =\n \"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about\";\n\n describe(\"seedToAccount\", () => {\n test(\"deterministic derivation from fixed mnemonic\", () => {\n const a = seedToAccount(TEST_MNEMONIC);\n const b = seedToAccount(TEST_MNEMONIC);\n expect(a.ss58Address).toBe(b.ss58Address);\n expect(a.h160Address).toBe(b.h160Address);\n expect(a.publicKey).toEqual(b.publicKey);\n expect(a.publicKey.length).toBe(32);\n });\n\n test(\"returns valid SS58 and H160 addresses\", () => {\n const account = seedToAccount(TEST_MNEMONIC);\n expect(account.ss58Address).toMatch(/^[1-9A-HJ-NP-Za-km-z]+$/);\n expect(account.h160Address).toMatch(/^0x[a-f0-9]{40}$/);\n });\n\n test(\"custom derivation path produces different addresses\", () => {\n const a = seedToAccount(TEST_MNEMONIC, \"//0\");\n const b = seedToAccount(TEST_MNEMONIC, \"//1\");\n expect(a.ss58Address).not.toBe(b.ss58Address);\n expect(a.h160Address).not.toBe(b.h160Address);\n });\n\n test(\"custom SS58 prefix changes address encoding\", () => {\n const generic = seedToAccount(TEST_MNEMONIC, \"//0\", 42);\n const polkadot = seedToAccount(TEST_MNEMONIC, \"//0\", 0);\n expect(generic.ss58Address).not.toBe(polkadot.ss58Address);\n // Same underlying public key\n expect(generic.publicKey).toEqual(polkadot.publicKey);\n expect(generic.h160Address).toBe(polkadot.h160Address);\n });\n\n test(\"provides a signer\", () => {\n const account = seedToAccount(TEST_MNEMONIC);\n expect(account.signer).toBeDefined();\n expect(account.signer.publicKey).toEqual(account.publicKey);\n });\n\n test(\"works with a freshly generated mnemonic\", () => {\n const mnemonic = generateMnemonic();\n const account = seedToAccount(mnemonic);\n expect(account.ss58Address).toBeTruthy();\n expect(account.publicKey.length).toBe(32);\n });\n\n test(\"throws descriptive error for invalid mnemonic\", () => {\n expect(() => seedToAccount(\"not a valid mnemonic\")).toThrow(\"Invalid mnemonic phrase\");\n });\n\n test(\"throws descriptive error for empty string\", () => {\n expect(() => seedToAccount(\"\")).toThrow(\"Invalid mnemonic phrase\");\n });\n\n test(\"ed25519 derivation produces different address than sr25519\", () => {\n const sr = seedToAccount(TEST_MNEMONIC, \"//0\", 42, \"sr25519\");\n const ed = seedToAccount(TEST_MNEMONIC, \"//0\", 42, \"ed25519\");\n expect(ed.ss58Address).not.toBe(sr.ss58Address);\n expect(ed.publicKey).not.toEqual(sr.publicKey);\n });\n\n test(\"ed25519 derivation is deterministic\", () => {\n const a = seedToAccount(TEST_MNEMONIC, \"//0\", 42, \"ed25519\");\n const b = seedToAccount(TEST_MNEMONIC, \"//0\", 42, \"ed25519\");\n expect(a.ss58Address).toBe(b.ss58Address);\n expect(a.h160Address).toBe(b.h160Address);\n expect(a.publicKey).toEqual(b.publicKey);\n expect(a.publicKey.length).toBe(32);\n });\n\n test(\"ed25519 provides a signer with matching publicKey\", () => {\n const account = seedToAccount(TEST_MNEMONIC, \"//0\", 42, \"ed25519\");\n expect(account.signer).toBeDefined();\n expect(account.signer.publicKey).toEqual(account.publicKey);\n });\n\n test(\"default keyType is sr25519 (backward compatible)\", () => {\n const withDefault = seedToAccount(TEST_MNEMONIC);\n const withExplicit = seedToAccount(TEST_MNEMONIC, \"//0\", 42, \"sr25519\");\n expect(withDefault.ss58Address).toBe(withExplicit.ss58Address);\n expect(withDefault.publicKey).toEqual(withExplicit.publicKey);\n });\n });\n}\n","// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\nimport { generateMnemonic } from \"@polkadot-labs/hdkd-helpers\";\nimport type { LocalKvStore } from \"@parity/product-sdk-local-storage\";\n\nimport { seedToAccount } from \"./seed-to-account.js\";\nimport type { SessionKeyInfo } from \"./types.js\";\n\n/**\n * Manages an sr25519 account derived from a BIP39 mnemonic.\n *\n * @param options.store - LocalKvStore instance (from `@parity/product-sdk-local-storage`).\n * Create with `createLocalKvStore({ prefix: \"session-key\" })` for namespaced persistence.\n * @param options.name - Identifies this session key. Defaults to `\"default\"`.\n * Use different names to manage multiple independent session keys.\n *\n * @example\n * ```ts\n * const store = await createLocalKvStore({ prefix: \"session-key\" });\n * const skm = new SessionKeyManager({ store });\n * const key = await skm.getOrCreate();\n * ```\n */\nexport class SessionKeyManager {\n private readonly name: string;\n private readonly store: LocalKvStore;\n\n constructor(options: { store: LocalKvStore; name?: string }) {\n this.name = options.name ?? \"default\";\n this.store = options.store;\n }\n\n /**\n * Create a new session key from a fresh mnemonic.\n * Persists the mnemonic to the store.\n */\n async create(): Promise<SessionKeyInfo> {\n const mnemonic = generateMnemonic();\n await this.store.set(this.name, mnemonic);\n return { mnemonic, account: seedToAccount(mnemonic) };\n }\n\n /**\n * Load an existing session key from the store.\n * Returns null if no mnemonic is stored.\n */\n async get(): Promise<SessionKeyInfo | null> {\n const mnemonic = await this.store.get(this.name);\n if (!mnemonic) return null;\n return { mnemonic, account: seedToAccount(mnemonic) };\n }\n\n /**\n * Load existing or create a new session key.\n */\n async getOrCreate(): Promise<SessionKeyInfo> {\n const existing = await this.get();\n if (existing) return existing;\n return this.create();\n }\n\n /**\n * Derive a session key from an explicit mnemonic (no storage interaction).\n */\n fromMnemonic(mnemonic: string): SessionKeyInfo {\n return { mnemonic, account: seedToAccount(mnemonic) };\n }\n\n /**\n * Clear the stored mnemonic from the store.\n */\n async clear(): Promise<void> {\n await this.store.remove(this.name);\n }\n}\n\nif (import.meta.vitest) {\n const { test, expect, describe } = import.meta.vitest;\n\n const TEST_MNEMONIC =\n \"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about\";\n\n function mockLocalKvStore(): LocalKvStore & { data: Map<string, string> } {\n const data = new Map<string, string>();\n return {\n data,\n async get(key) {\n return data.get(key) ?? null;\n },\n async set(key, value) {\n data.set(key, value);\n },\n async remove(key) {\n data.delete(key);\n },\n async getJSON() {\n return null;\n },\n async setJSON() {},\n };\n }\n\n describe(\"SessionKeyManager\", () => {\n test(\"fromMnemonic produces deterministic results\", () => {\n const store = mockLocalKvStore();\n const skm = new SessionKeyManager({ store });\n const a = skm.fromMnemonic(TEST_MNEMONIC);\n const b = skm.fromMnemonic(TEST_MNEMONIC);\n expect(a.mnemonic).toBe(TEST_MNEMONIC);\n expect(a.account.ss58Address).toBe(b.account.ss58Address);\n expect(a.account.h160Address).toBe(b.account.h160Address);\n });\n\n test(\"fromMnemonic throws on invalid mnemonic\", () => {\n const store = mockLocalKvStore();\n const skm = new SessionKeyManager({ store });\n expect(() => skm.fromMnemonic(\"invalid words here\")).toThrow(\"Invalid mnemonic phrase\");\n });\n\n test(\"create and get round-trip\", async () => {\n const store = mockLocalKvStore();\n const skm = new SessionKeyManager({ store });\n const info = await skm.create();\n expect(info.mnemonic).toBeTruthy();\n expect(info.account.ss58Address).toMatch(/^[1-9A-HJ-NP-Za-km-z]+$/);\n expect(store.data.get(\"default\")).toBe(info.mnemonic);\n\n const loaded = await skm.get();\n expect(loaded?.mnemonic).toBe(info.mnemonic);\n });\n\n test(\"get returns null when no key stored\", async () => {\n const store = mockLocalKvStore();\n const skm = new SessionKeyManager({ store });\n expect(await skm.get()).toBeNull();\n });\n\n test(\"getOrCreate creates then returns cached\", async () => {\n const store = mockLocalKvStore();\n const skm = new SessionKeyManager({ store });\n const created = await skm.getOrCreate();\n expect(store.data.size).toBe(1);\n\n const loaded = await skm.getOrCreate();\n expect(loaded.mnemonic).toBe(created.mnemonic);\n expect(loaded.account.ss58Address).toBe(created.account.ss58Address);\n });\n\n test(\"clear removes mnemonic from store\", async () => {\n const store = mockLocalKvStore();\n const skm = new SessionKeyManager({ store });\n await skm.create();\n expect(store.data.size).toBe(1);\n\n await skm.clear();\n expect(store.data.size).toBe(0);\n expect(await skm.get()).toBeNull();\n });\n\n test(\"name separates storage keys\", async () => {\n const store = mockLocalKvStore();\n const main = new SessionKeyManager({ name: \"main\", store });\n const burner = new SessionKeyManager({ name: \"burner\", store });\n\n const mainInfo = await main.create();\n const burnerInfo = await burner.create();\n\n expect(store.data.get(\"main\")).toBe(mainInfo.mnemonic);\n expect(store.data.get(\"burner\")).toBe(burnerInfo.mnemonic);\n expect(mainInfo.account.ss58Address).not.toBe(burnerInfo.account.ss58Address);\n\n await main.clear();\n expect(store.data.has(\"main\")).toBe(false);\n expect(store.data.get(\"burner\")).toBe(burnerInfo.mnemonic);\n });\n });\n}\n","// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\n/**\n * Canonical sr25519 product-account public-key derivation.\n *\n * Mirrored byte-for-byte by polkadot-desktop\n * (`polkadot-desktop/src/domains/product/account/service.ts`) and conceptually\n * by polkadot-app-android-v2\n * (`feature/products/impl/.../ProductAccountDerivationUseCase.kt`).\n *\n * The function works on the parent *public* key alone: sr25519 soft derivation\n * is composable on public keys, so the CLI / web host / any external client can\n * compute the same derived address that the mobile wallet derives privately,\n * without ever seeing the secret key.\n *\n * Junction path: [\"product\", productId, String(derivationIndex)], applied\n * left-to-right. For each junction, a 32-byte chain code is built:\n * - numeric (\"^\\d+$\") -> SCALE u64 (BigInt), zero-padded to 32 bytes\n * - string -> SCALE str (compact-length + UTF-8), zero-padded\n * - if encoded > 32 bytes -> blake2b256(encoded) (32-byte BLAKE2b digest)\n *\n * # productId constraint (cross-platform parity)\n *\n * `productId` MUST contain at least one non-hex character or be of odd\n * length when serialized as a string. polkadot-app-android-v2's\n * SubstrateJunctionDecoder tries to interpret a junction as hex BEFORE\n * falling through to SCALE-string encoding; polkadot-desktop and this\n * implementation skip that hex branch. For productIds that happen to be\n * even-length all-hex strings (e.g. \"deadbeef\", \"c0ffee01\"), Android would\n * derive a different public key than desktop or this implementation. In\n * practice, productIds are always dotNS names (e.g. \"playground.dot\"),\n * which contain \".\" and therefore never trip the hex branch on Android.\n */\n\nimport { blake2b256 } from \"@parity/product-sdk-crypto\";\nimport { HDKD } from \"@scure/sr25519\";\nimport { str, u64 } from \"scale-ts\";\n\nconst JUNCTION_ID_LEN = 32;\nconst NON_NEGATIVE_INTEGER = /^\\d+$/;\n\nexport function createChainCode(code: string): Uint8Array {\n const encoded = NON_NEGATIVE_INTEGER.test(code) ? u64.enc(BigInt(code)) : str.enc(code);\n\n if (encoded.length > JUNCTION_ID_LEN) {\n return blake2b256(encoded);\n }\n\n const chainCode = new Uint8Array(JUNCTION_ID_LEN);\n chainCode.set(encoded);\n return chainCode;\n}\n\nexport function deriveProductAccountPublicKey(\n parentPublicKey: Uint8Array,\n productId: string,\n derivationIndex: number,\n): Uint8Array {\n const junctions = [\"product\", productId, String(derivationIndex)];\n return junctions.reduce<Uint8Array>(\n (pubkey, junction) => HDKD.publicSoft(pubkey, createChainCode(junction)),\n parentPublicKey,\n );\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parity/product-sdk-keys",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Hierarchical key derivation and session key management for Polkadot accounts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
"polkadot-api": "^2.1.2",
|
|
25
25
|
"scale-ts": "^1.6.1",
|
|
26
26
|
"@parity/product-sdk-address": "0.1.1",
|
|
27
|
-
"@parity/product-sdk-
|
|
28
|
-
"@parity/product-sdk-
|
|
27
|
+
"@parity/product-sdk-local-storage": "0.2.1",
|
|
28
|
+
"@parity/product-sdk-crypto": "0.1.1"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"typescript": "^5.7.0",
|
package/src/index.ts
CHANGED
package/src/key-manager.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Copyright 2026 Parity Technologies (UK) Ltd.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
import { getPublicKey, secretFromSeed } from "@scure/sr25519";
|
|
2
4
|
import { describe, expect, it } from "vitest";
|
|
3
5
|
import { createChainCode, deriveProductAccountPublicKey } from "./product-account.js";
|
package/src/product-account.ts
CHANGED
package/src/seed-to-account.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Copyright 2026 Parity Technologies (UK) Ltd.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
import { ed25519CreateDerive, sr25519CreateDerive } from "@polkadot-labs/hdkd";
|
|
2
4
|
import { entropyToMiniSecret, mnemonicToEntropy } from "@polkadot-labs/hdkd-helpers";
|
|
3
5
|
import { getPolkadotSigner } from "polkadot-api/signer";
|