@mysten/sui
Version:
Sui TypeScript API
234 lines (232 loc) • 9.18 kB
JavaScript
import { PasskeyAuthenticator } from "../../bcs/bcs.mjs";
import { messageWithIntent } from "../../cryptography/intent.mjs";
import { SIGNATURE_SCHEME_TO_FLAG } from "../../cryptography/signature-scheme.mjs";
import { PASSKEY_PUBLIC_KEY_SIZE, PASSKEY_SIGNATURE_SIZE, PasskeyPublicKey, parseDerSPKI } from "./publickey.mjs";
import { Signer } from "../../cryptography/keypair.mjs";
import { toBase64 } from "@mysten/bcs";
import { blake2b } from "@noble/hashes/blake2.js";
import { p256 } from "@noble/curves/nist.js";
import { sha256 } from "@noble/hashes/sha2.js";
import { randomBytes } from "@noble/hashes/utils.js";
//#region src/keypairs/passkey/keypair.ts
var BrowserPasskeyProvider = class {
#name;
#options;
constructor(name, options) {
this.#name = name;
this.#options = options;
}
async create() {
return await navigator.credentials.create({ publicKey: {
timeout: this.#options.timeout ?? 6e4,
...this.#options,
rp: {
name: this.#name,
...this.#options.rp
},
user: {
name: this.#name,
displayName: this.#name,
...this.#options.user,
id: randomBytes(10)
},
challenge: new TextEncoder().encode("Create passkey wallet on Sui"),
pubKeyCredParams: [{
alg: -7,
type: "public-key"
}],
authenticatorSelection: {
authenticatorAttachment: "cross-platform",
residentKey: "required",
requireResidentKey: true,
userVerification: "required",
...this.#options.authenticatorSelection
}
} });
}
async get(challenge, credentialId) {
return await navigator.credentials.get({ publicKey: {
challenge,
userVerification: this.#options.authenticatorSelection?.userVerification || "required",
timeout: this.#options.timeout ?? 6e4,
...credentialId && { allowCredentials: [{
type: "public-key",
id: credentialId
}] }
} });
}
};
/**
* @experimental
* 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).
*/
var PasskeyKeypair = class PasskeyKeypair extends Signer {
/**
* Get the key scheme of passkey,
*/
getKeyScheme() {
return "Passkey";
}
/**
* Creates an instance of Passkey signer. If no passkey wallet had created before,
* use `getPasskeyInstance`. For example:
* ```
* let provider = new BrowserPasskeyProvider('Sui Passkey Example',{
* rpName: 'Sui Passkey Example',
* rpId: window.location.hostname,
* } as BrowserPasswordProviderOptions);
* const signer = await PasskeyKeypair.getPasskeyInstance(provider);
* ```
*
* If there are existing passkey wallet, use `signAndRecover` to identify the correct
* public key and then initialize the instance. See usage in `signAndRecover`.
*/
constructor(publicKey, provider, credentialId) {
super();
this.publicKey = publicKey;
this.provider = provider;
this.credentialId = credentialId;
}
/**
* Return the credential ID for this passkey, if available.
* The credential ID is captured when creating a new passkey via `getPasskeyInstance`
* and can be used to constrain which credential the browser selects during signing.
*/
getCredentialId() {
return this.credentialId;
}
/**
* Creates an instance of Passkey signer invoking the passkey from navigator.
* Note that this will invoke the passkey device to create a fresh credential.
* Should only be called if passkey wallet is created for the first time.
*
* @param provider - the passkey provider.
* @returns the passkey instance.
*/
static async getPasskeyInstance(provider) {
const credential = await provider.create();
if (!credential.response.getPublicKey()) throw new Error("Invalid credential create response");
else {
const derSPKI = credential.response.getPublicKey();
const pubkeyUncompressed = parseDerSPKI(new Uint8Array(derSPKI));
return new PasskeyKeypair(p256.Point.fromBytes(pubkeyUncompressed).toBytes(true), provider, new Uint8Array(credential.rawId));
}
}
/**
* Return the public key for this passkey.
*/
getPublicKey() {
return new PasskeyPublicKey(this.publicKey);
}
/**
* Return the signature for the provided data (i.e. blake2b(intent_message)).
* This is sent to passkey as the challenge field.
*/
async sign(data) {
const credential = await this.provider.get(data, this.credentialId);
const authenticatorData = new Uint8Array(credential.response.authenticatorData);
const clientDataJSON = new Uint8Array(credential.response.clientDataJSON);
const clientDataJSONString = new TextDecoder().decode(clientDataJSON);
const sig = p256.Signature.fromBytes(new Uint8Array(credential.response.signature), "der");
const normalized = (sig.hasHighS() ? new p256.Signature(sig.r, p256.Point.Fn.neg(sig.s)) : sig).toBytes("compact");
if (normalized.length !== PASSKEY_SIGNATURE_SIZE || this.publicKey.length !== PASSKEY_PUBLIC_KEY_SIZE) throw new Error("Invalid signature or public key length");
const arr = new Uint8Array(1 + normalized.length + this.publicKey.length);
arr.set([SIGNATURE_SCHEME_TO_FLAG["Secp256r1"]]);
arr.set(normalized, 1);
arr.set(this.publicKey, 1 + normalized.length);
return PasskeyAuthenticator.serialize({
authenticatorData,
clientDataJson: clientDataJSONString,
userSignature: arr
}).toBytes();
}
/**
* This overrides the base class implementation that accepts the raw bytes and signs its
* digest of the intent message, then serialize it with the passkey flag.
*/
async signWithIntent(bytes, intent) {
const digest = blake2b(messageWithIntent(intent, bytes), { dkLen: 32 });
const signature = await this.sign(digest);
const serializedSignature = new Uint8Array(1 + signature.length);
serializedSignature.set([SIGNATURE_SCHEME_TO_FLAG[this.getKeyScheme()]]);
serializedSignature.set(signature, 1);
return {
signature: toBase64(serializedSignature),
bytes: toBase64(bytes)
};
}
/**
* Given a message, asks the passkey device to sign it and return all (up to 4) possible public keys.
* See: https://bitcoin.stackexchange.com/questions/81232/how-is-public-key-extracted-from-message-digital-signature-address
*
* This is useful if the user previously created passkey wallet with the origin, but the wallet session
* does not have the public key / address. By calling this method twice with two different messages, the
* wallet can compare the returned public keys and uniquely identify the previously created passkey wallet
* using `findCommonPublicKey`.
*
* Alternatively, one call can be made and all possible public keys should be checked onchain to see if
* there is any assets.
*
* Once the correct public key is identified, a passkey instance can then be initialized with this public key.
*
* Example usage to recover wallet with two signing calls:
* ```
* let provider = new BrowserPasskeyProvider('Sui Passkey Example',{
* rpName: 'Sui Passkey Example',
* rpId: window.location.hostname,
* } as BrowserPasswordProviderOptions);
* const testMessage = new TextEncoder().encode('Hello world!');
* const possiblePks = await PasskeyKeypair.signAndRecover(provider, testMessage);
* const testMessage2 = new TextEncoder().encode('Hello world 2!');
* const possiblePks2 = await PasskeyKeypair.signAndRecover(provider, testMessage2);
* const commonPk = findCommonPublicKey(possiblePks, possiblePks2);
* const signer = new PasskeyKeypair(commonPk.toRawBytes(), provider);
* ```
*
* @param provider - the passkey provider.
* @param message - the message to sign.
* @returns all possible public keys.
*/
static async signAndRecover(provider, message) {
const credential = await provider.get(message);
const fullMessage = messageFromAssertionResponse(credential.response);
const sig = p256.Signature.fromBytes(new Uint8Array(credential.response.signature), "der");
const res = [];
const msgHash = sha256(fullMessage);
for (let i = 0; i < 4; i++) {
const s = sig.addRecoveryBit(i);
try {
const pk = new PasskeyPublicKey(s.recoverPublicKey(msgHash).toBytes(true));
res.push(pk);
} catch {
continue;
}
}
return res;
}
};
/**
* Finds the unique public key that exists in both arrays, throws error if the common
* pubkey does not equal to one.
*
* @param arr1 - The first pubkeys array.
* @param arr2 - The second pubkeys array.
* @returns The only common pubkey in both arrays.
*/
function findCommonPublicKey(arr1, arr2) {
const matchingPubkeys = [];
for (const pubkey1 of arr1) for (const pubkey2 of arr2) if (pubkey1.equals(pubkey2)) matchingPubkeys.push(pubkey1);
if (matchingPubkeys.length !== 1) throw new Error("No unique public key found");
return matchingPubkeys[0];
}
/**
* Constructs the message that the passkey signature is produced over as authenticatorData || sha256(clientDataJSON).
*/
function messageFromAssertionResponse(response) {
const authenticatorData = new Uint8Array(response.authenticatorData);
const clientDataJSONDigest = sha256(new Uint8Array(response.clientDataJSON));
return new Uint8Array([...authenticatorData, ...clientDataJSONDigest]);
}
//#endregion
export { BrowserPasskeyProvider, PasskeyKeypair, findCommonPublicKey };
//# sourceMappingURL=keypair.mjs.map