UNPKG

@mysten/sui

Version:
1 lines 15.9 kB
{"version":3,"file":"keypair.mjs","names":["#name","#options","secp256r1"],"sources":["../../../src/keypairs/passkey/keypair.ts"],"sourcesContent":["// Copyright (c) Mysten Labs, Inc.\n// SPDX-License-Identifier: Apache-2.0\n\nimport { toBase64 } from '@mysten/bcs';\nimport { p256 as secp256r1 } from '@noble/curves/nist.js';\nimport { blake2b } from '@noble/hashes/blake2.js';\nimport { sha256 } from '@noble/hashes/sha2.js';\nimport { randomBytes } from '@noble/hashes/utils.js';\n\nimport { PasskeyAuthenticator } from '../../bcs/bcs.js';\nimport type { IntentScope, SignatureWithBytes } from '../../cryptography/index.js';\nimport { messageWithIntent, SIGNATURE_SCHEME_TO_FLAG, Signer } from '../../cryptography/index.js';\nimport type { PublicKey } from '../../cryptography/publickey.js';\nimport type { SignatureScheme } from '../../cryptography/signature-scheme.js';\nimport {\n\tparseDerSPKI,\n\tPASSKEY_PUBLIC_KEY_SIZE,\n\tPASSKEY_SIGNATURE_SIZE,\n\tPasskeyPublicKey,\n} from './publickey.js';\nimport type { AuthenticationCredential, RegistrationCredential } from './types.js';\n\ntype DeepPartialConfigKeys = 'rp' | 'user' | 'authenticatorSelection';\n\ntype DeepPartial<T> = T extends object\n\t? {\n\t\t\t[P in keyof T]?: DeepPartial<T[P]>;\n\t\t}\n\t: T;\n\nexport type BrowserPasswordProviderOptions = Pick<\n\tDeepPartial<PublicKeyCredentialCreationOptions>,\n\tDeepPartialConfigKeys\n> &\n\tOmit<\n\t\tPartial<PublicKeyCredentialCreationOptions>,\n\t\tDeepPartialConfigKeys | 'pubKeyCredParams' | 'challenge'\n\t>;\n\nexport interface PasskeyProvider {\n\tcreate(): Promise<RegistrationCredential>;\n\tget(challenge: Uint8Array, credentialId?: Uint8Array): Promise<AuthenticationCredential>;\n}\n\n// Default browser implementation\nexport class BrowserPasskeyProvider implements PasskeyProvider {\n\t#name: string;\n\t#options: BrowserPasswordProviderOptions;\n\n\tconstructor(name: string, options: BrowserPasswordProviderOptions) {\n\t\tthis.#name = name;\n\t\tthis.#options = options;\n\t}\n\n\tasync create(): Promise<RegistrationCredential> {\n\t\treturn (await navigator.credentials.create({\n\t\t\tpublicKey: {\n\t\t\t\ttimeout: this.#options.timeout ?? 60000,\n\t\t\t\t...this.#options,\n\t\t\t\trp: {\n\t\t\t\t\tname: this.#name,\n\t\t\t\t\t...this.#options.rp,\n\t\t\t\t},\n\t\t\t\tuser: {\n\t\t\t\t\tname: this.#name,\n\t\t\t\t\tdisplayName: this.#name,\n\t\t\t\t\t...this.#options.user,\n\t\t\t\t\tid: randomBytes(10) as BufferSource,\n\t\t\t\t},\n\t\t\t\tchallenge: new TextEncoder().encode('Create passkey wallet on Sui'),\n\t\t\t\tpubKeyCredParams: [{ alg: -7, type: 'public-key' }],\n\t\t\t\tauthenticatorSelection: {\n\t\t\t\t\tauthenticatorAttachment: 'cross-platform',\n\t\t\t\t\tresidentKey: 'required',\n\t\t\t\t\trequireResidentKey: true,\n\t\t\t\t\tuserVerification: 'required',\n\t\t\t\t\t...this.#options.authenticatorSelection,\n\t\t\t\t},\n\t\t\t},\n\t\t})) as RegistrationCredential;\n\t}\n\n\tasync get(challenge: Uint8Array, credentialId?: Uint8Array): Promise<AuthenticationCredential> {\n\t\treturn (await navigator.credentials.get({\n\t\t\tpublicKey: {\n\t\t\t\tchallenge: challenge as BufferSource,\n\t\t\t\tuserVerification: this.#options.authenticatorSelection?.userVerification || 'required',\n\t\t\t\ttimeout: this.#options.timeout ?? 60000,\n\t\t\t\t...(credentialId && {\n\t\t\t\t\tallowCredentials: [{ type: 'public-key' as const, id: credentialId as BufferSource }],\n\t\t\t\t}),\n\t\t\t},\n\t\t})) as AuthenticationCredential;\n\t}\n}\n\n/**\n * @experimental\n * A passkey signer used for signing transactions. This is a client side implementation for [SIP-9](https://github.com/sui-foundation/sips/blob/main/sips/sip-9.md).\n */\nexport class PasskeyKeypair extends Signer {\n\tprivate publicKey: Uint8Array;\n\tprivate provider: PasskeyProvider;\n\tprivate credentialId?: Uint8Array;\n\n\t/**\n\t * Get the key scheme of passkey,\n\t */\n\tgetKeyScheme(): SignatureScheme {\n\t\treturn 'Passkey';\n\t}\n\n\t/**\n\t * Creates an instance of Passkey signer. If no passkey wallet had created before,\n\t * use `getPasskeyInstance`. For example:\n\t * ```\n\t * let provider = new BrowserPasskeyProvider('Sui Passkey Example',{\n\t * \t rpName: 'Sui Passkey Example',\n\t * \t rpId: window.location.hostname,\n\t * } as BrowserPasswordProviderOptions);\n\t * const signer = await PasskeyKeypair.getPasskeyInstance(provider);\n\t * ```\n\t *\n\t * If there are existing passkey wallet, use `signAndRecover` to identify the correct\n\t * public key and then initialize the instance. See usage in `signAndRecover`.\n\t */\n\tconstructor(publicKey: Uint8Array, provider: PasskeyProvider, credentialId?: Uint8Array) {\n\t\tsuper();\n\t\tthis.publicKey = publicKey;\n\t\tthis.provider = provider;\n\t\tthis.credentialId = credentialId;\n\t}\n\n\t/**\n\t * Return the credential ID for this passkey, if available.\n\t * The credential ID is captured when creating a new passkey via `getPasskeyInstance`\n\t * and can be used to constrain which credential the browser selects during signing.\n\t */\n\tgetCredentialId(): Uint8Array | undefined {\n\t\treturn this.credentialId;\n\t}\n\n\t/**\n\t * Creates an instance of Passkey signer invoking the passkey from navigator.\n\t * Note that this will invoke the passkey device to create a fresh credential.\n\t * Should only be called if passkey wallet is created for the first time.\n\t *\n\t * @param provider - the passkey provider.\n\t * @returns the passkey instance.\n\t */\n\tstatic async getPasskeyInstance(provider: PasskeyProvider): Promise<PasskeyKeypair> {\n\t\t// create a passkey secp256r1 with the provider.\n\t\tconst credential = await provider.create();\n\n\t\tif (!credential.response.getPublicKey()) {\n\t\t\tthrow new Error('Invalid credential create response');\n\t\t} else {\n\t\t\tconst derSPKI = credential.response.getPublicKey()!;\n\t\t\tconst pubkeyUncompressed = parseDerSPKI(new Uint8Array(derSPKI));\n\t\t\tconst pubkey = secp256r1.Point.fromBytes(pubkeyUncompressed);\n\t\t\tconst pubkeyCompressed = pubkey.toBytes(true);\n\t\t\treturn new PasskeyKeypair(pubkeyCompressed, provider, new Uint8Array(credential.rawId));\n\t\t}\n\t}\n\n\t/**\n\t * Return the public key for this passkey.\n\t */\n\tgetPublicKey(): PublicKey {\n\t\treturn new PasskeyPublicKey(this.publicKey);\n\t}\n\n\t/**\n\t * Return the signature for the provided data (i.e. blake2b(intent_message)).\n\t * This is sent to passkey as the challenge field.\n\t */\n\tasync sign(data: Uint8Array) {\n\t\t// asks the passkey to sign over challenge as the data.\n\t\tconst credential = await this.provider.get(data, this.credentialId);\n\n\t\t// parse authenticatorData (as bytes), clientDataJSON (decoded as string).\n\t\tconst authenticatorData = new Uint8Array(credential.response.authenticatorData);\n\t\tconst clientDataJSON = new Uint8Array(credential.response.clientDataJSON); // response.clientDataJSON is already UTF-8 encoded JSON\n\t\tconst decoder = new TextDecoder();\n\t\tconst clientDataJSONString: string = decoder.decode(clientDataJSON);\n\n\t\tconst sig = secp256r1.Signature.fromBytes(new Uint8Array(credential.response.signature), 'der');\n\t\tconst normalizedSig = sig.hasHighS()\n\t\t\t? new secp256r1.Signature(sig.r, secp256r1.Point.Fn.neg(sig.s))\n\t\t\t: sig;\n\n\t\tconst normalized = normalizedSig.toBytes('compact');\n\n\t\tif (\n\t\t\tnormalized.length !== PASSKEY_SIGNATURE_SIZE ||\n\t\t\tthis.publicKey.length !== PASSKEY_PUBLIC_KEY_SIZE\n\t\t) {\n\t\t\tthrow new Error('Invalid signature or public key length');\n\t\t}\n\n\t\t// construct userSignature as flag || sig || pubkey for the secp256r1 signature.\n\t\tconst arr = new Uint8Array(1 + normalized.length + this.publicKey.length);\n\t\tarr.set([SIGNATURE_SCHEME_TO_FLAG['Secp256r1']]);\n\t\tarr.set(normalized, 1);\n\t\tarr.set(this.publicKey, 1 + normalized.length);\n\n\t\t// serialize all fields into a passkey signature according to https://github.com/sui-foundation/sips/blob/main/sips/sip-9.md#signature-encoding\n\t\treturn PasskeyAuthenticator.serialize({\n\t\t\tauthenticatorData: authenticatorData,\n\t\t\tclientDataJson: clientDataJSONString,\n\t\t\tuserSignature: arr,\n\t\t}).toBytes();\n\t}\n\n\t/**\n\t * This overrides the base class implementation that accepts the raw bytes and signs its\n\t * digest of the intent message, then serialize it with the passkey flag.\n\t */\n\tasync signWithIntent(bytes: Uint8Array, intent: IntentScope): Promise<SignatureWithBytes> {\n\t\t// prepend it into an intent message and computes the digest.\n\t\tconst intentMessage = messageWithIntent(intent, bytes);\n\t\tconst digest = blake2b(intentMessage, { dkLen: 32 });\n\n\t\t// sign the digest.\n\t\tconst signature = await this.sign(digest);\n\n\t\t// prepend with the passkey flag.\n\t\tconst serializedSignature = new Uint8Array(1 + signature.length);\n\t\tserializedSignature.set([SIGNATURE_SCHEME_TO_FLAG[this.getKeyScheme()]]);\n\t\tserializedSignature.set(signature, 1);\n\t\treturn {\n\t\t\tsignature: toBase64(serializedSignature),\n\t\t\tbytes: toBase64(bytes),\n\t\t};\n\t}\n\n\t/**\n\t * Given a message, asks the passkey device to sign it and return all (up to 4) possible public keys.\n\t * See: https://bitcoin.stackexchange.com/questions/81232/how-is-public-key-extracted-from-message-digital-signature-address\n\t *\n\t * This is useful if the user previously created passkey wallet with the origin, but the wallet session\n\t * does not have the public key / address. By calling this method twice with two different messages, the\n\t * wallet can compare the returned public keys and uniquely identify the previously created passkey wallet\n\t * using `findCommonPublicKey`.\n\t *\n\t * Alternatively, one call can be made and all possible public keys should be checked onchain to see if\n\t * there is any assets.\n\t *\n\t * Once the correct public key is identified, a passkey instance can then be initialized with this public key.\n\t *\n\t * Example usage to recover wallet with two signing calls:\n\t * ```\n\t * let provider = new BrowserPasskeyProvider('Sui Passkey Example',{\n\t * rpName: 'Sui Passkey Example',\n\t * \t rpId: window.location.hostname,\n\t * } as BrowserPasswordProviderOptions);\n\t * const testMessage = new TextEncoder().encode('Hello world!');\n\t * const possiblePks = await PasskeyKeypair.signAndRecover(provider, testMessage);\n\t * const testMessage2 = new TextEncoder().encode('Hello world 2!');\n\t * const possiblePks2 = await PasskeyKeypair.signAndRecover(provider, testMessage2);\n\t * const commonPk = findCommonPublicKey(possiblePks, possiblePks2);\n\t * const signer = new PasskeyKeypair(commonPk.toRawBytes(), provider);\n\t * ```\n\t *\n\t * @param provider - the passkey provider.\n\t * @param message - the message to sign.\n\t * @returns all possible public keys.\n\t */\n\tstatic async signAndRecover(\n\t\tprovider: PasskeyProvider,\n\t\tmessage: Uint8Array,\n\t): Promise<PublicKey[]> {\n\t\tconst credential = await provider.get(message);\n\t\tconst fullMessage = messageFromAssertionResponse(credential.response);\n\t\tconst sig = secp256r1.Signature.fromBytes(new Uint8Array(credential.response.signature), 'der');\n\n\t\tconst res = [];\n\t\tconst msgHash = sha256(fullMessage);\n\t\tfor (let i = 0; i < 4; i++) {\n\t\t\tconst s = sig.addRecoveryBit(i);\n\t\t\ttry {\n\t\t\t\tconst pubkey = s.recoverPublicKey(msgHash);\n\t\t\t\tconst pk = new PasskeyPublicKey(pubkey.toBytes(true));\n\t\t\t\tres.push(pk);\n\t\t\t} catch {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\t\treturn res;\n\t}\n}\n\n/**\n * Finds the unique public key that exists in both arrays, throws error if the common\n * pubkey does not equal to one.\n *\n * @param arr1 - The first pubkeys array.\n * @param arr2 - The second pubkeys array.\n * @returns The only common pubkey in both arrays.\n */\nexport function findCommonPublicKey(arr1: PublicKey[], arr2: PublicKey[]): PublicKey {\n\tconst matchingPubkeys: PublicKey[] = [];\n\tfor (const pubkey1 of arr1) {\n\t\tfor (const pubkey2 of arr2) {\n\t\t\tif (pubkey1.equals(pubkey2)) {\n\t\t\t\tmatchingPubkeys.push(pubkey1);\n\t\t\t}\n\t\t}\n\t}\n\tif (matchingPubkeys.length !== 1) {\n\t\tthrow new Error('No unique public key found');\n\t}\n\treturn matchingPubkeys[0];\n}\n\n/**\n * Constructs the message that the passkey signature is produced over as authenticatorData || sha256(clientDataJSON).\n */\nfunction messageFromAssertionResponse(response: AuthenticatorAssertionResponse): Uint8Array {\n\tconst authenticatorData = new Uint8Array(response.authenticatorData);\n\tconst clientDataJSON = new Uint8Array(response.clientDataJSON);\n\tconst clientDataJSONDigest = sha256(clientDataJSON);\n\treturn new Uint8Array([...authenticatorData, ...clientDataJSONDigest]);\n}\n"],"mappings":"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"}