diff --git a/packages/protocol-kit/package.json b/packages/protocol-kit/package.json index 7c26a1ea3..1c57b4541 100644 --- a/packages/protocol-kit/package.json +++ b/packages/protocol-kit/package.json @@ -66,7 +66,9 @@ "web3": "^4.12.1" }, "dependencies": { + "@noble/curves": "^1.6.0", "@noble/hashes": "^1.3.3", + "@peculiar/asn1-schema": "^2.3.13", "@safe-global/safe-deployments": "^1.37.14", "@safe-global/safe-modules-deployments": "^2.2.4", "@safe-global/types-kit": "^1.0.0", diff --git a/packages/protocol-kit/src/Safe.ts b/packages/protocol-kit/src/Safe.ts index 3b9dd9859..ee63baeb0 100644 --- a/packages/protocol-kit/src/Safe.ts +++ b/packages/protocol-kit/src/Safe.ts @@ -42,7 +42,8 @@ import { SigningMethodType, SwapOwnerTxParams, SafeModulesPaginated, - RemovePasskeyOwnerTxParams + RemovePasskeyOwnerTxParams, + PasskeyCoordinates } from './types' import { EthSafeSignature, @@ -59,7 +60,8 @@ import { generateSignature, preimageSafeMessageHash, preimageSafeTransactionHash, - adjustVInSignature + adjustVInSignature, + extractPasskeyData } from './utils' import EthSafeTransaction from './utils/transactions/SafeTransaction' import { SafeTransactionOptionalProps } from './utils/transactions/types' @@ -1698,6 +1700,12 @@ class Safe { }): ContractInfo | undefined => { return getContractInfo(contractAddress) } + + static createPasskeySigner = async ( + credentials: Credential + ): Promise<{ rawId: string; coordinates: PasskeyCoordinates }> => { + return extractPasskeyData(credentials) + } } export default Safe diff --git a/packages/protocol-kit/src/index.ts b/packages/protocol-kit/src/index.ts index cd12b85cf..9c502e2e0 100644 --- a/packages/protocol-kit/src/index.ts +++ b/packages/protocol-kit/src/index.ts @@ -35,7 +35,6 @@ import { estimateTxGas, estimateSafeTxGas, estimateSafeDeploymentGas, - extractPasskeyCoordinates, extractPasskeyData, validateEthereumAddress, validateEip3770Address @@ -74,7 +73,6 @@ export { estimateSafeTxGas, estimateSafeDeploymentGas, extractPasskeyData, - extractPasskeyCoordinates, ContractManager, CreateCallBaseContract, createERC20TokenTransferTransaction, diff --git a/packages/protocol-kit/src/utils/passkeys/extractPasskeyData.ts b/packages/protocol-kit/src/utils/passkeys/extractPasskeyData.ts index 5760fd4b6..46fbb8645 100644 --- a/packages/protocol-kit/src/utils/passkeys/extractPasskeyData.ts +++ b/packages/protocol-kit/src/utils/passkeys/extractPasskeyData.ts @@ -1,6 +1,26 @@ -import { getFCLP256VerifierDeployment } from '@safe-global/safe-modules-deployments' +import { p256 } from '@noble/curves/p256' +import { AsnProp, AsnPropTypes, AsnType, AsnTypeTypes, AsnParser } from '@peculiar/asn1-schema' import { Buffer } from 'buffer' -import { PasskeyCoordinates, PasskeyArgType } from '@safe-global/protocol-kit/types' +import { getFCLP256VerifierDeployment } from '@safe-global/safe-modules-deployments' +import { PasskeyArgType, PasskeyCoordinates } from '@safe-global/protocol-kit/types' + +@AsnType({ type: AsnTypeTypes.Sequence }) +class AlgorithmIdentifier { + @AsnProp({ type: AsnPropTypes.ObjectIdentifier }) + public id: string = '' + + @AsnProp({ type: AsnPropTypes.ObjectIdentifier, optional: true }) + public curve: string = '' +} + +@AsnType({ type: AsnTypeTypes.Sequence }) +class ECPublicKey { + @AsnProp({ type: AlgorithmIdentifier }) + public algorithm = new AlgorithmIdentifier() + + @AsnProp({ type: AsnPropTypes.BitString }) + public publicKey: ArrayBuffer = new ArrayBuffer(0) +} /** * Extracts and returns the passkey data (coordinates and rawId) from a given passkey Credential. @@ -13,13 +33,7 @@ export async function extractPasskeyData(passkeyCredential: Credential): Promise const passkey = passkeyCredential as PublicKeyCredential const attestationResponse = passkey.response as AuthenticatorAttestationResponse - const publicKey = attestationResponse.getPublicKey() - - if (!publicKey) { - throw new Error('Failed to generate passkey Coordinates. getPublicKey() failed') - } - - const coordinates = await extractPasskeyCoordinates(publicKey) + const coordinates = decodePublicKey(attestationResponse) const rawId = Buffer.from(passkey.rawId).toString('hex') return { @@ -28,35 +42,74 @@ export async function extractPasskeyData(passkeyCredential: Credential): Promise } } +function isBase64String(str: string): boolean { + const base64Regex = /^(?:[A-Za-z0-9+\/]{4})*?(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/ + return base64Regex.test(str) +} + +function decodeBase64(base64: string): Uint8Array { + const binaryString = atob(base64.replace(/-/g, '+').replace(/_/g, '/')) + return Uint8Array.from(binaryString, (c) => c.charCodeAt(0)) +} + /** - * Extracts and returns coordinates from a given passkey public key. + * Decodes the x and y coordinates of the public key from a created public key credential response. + * Inspired from . * - * @param {ArrayBuffer} publicKey - The public key of the passkey from which coordinates will be extracted. - * @returns {Promise} A promise that resolves to an object containing the coordinates derived from the public key of the passkey. - * @throws {Error} Throws an error if the coordinates could not be extracted via `crypto.subtle.exportKey()` + * @param {Pick} response + * @returns {PasskeyCoordinates} Object containing the coordinates derived from the public key of the passkey. + * @throws {Error} Throws an error if the coordinates could not be extracted via `p256.ProjectivePoint.fromHex` */ -export async function extractPasskeyCoordinates( - publicKey: ArrayBuffer -): Promise { - const algorithm = { - name: 'ECDSA', - namedCurve: 'P-256', - hash: { name: 'SHA-256' } +export function decodePublicKey(response: AuthenticatorAttestationResponse): PasskeyCoordinates { + const publicKey = response.getPublicKey() + + if (!publicKey) { + throw new Error('Failed to generate passkey coordinates. getPublicKey() failed') } - const key = await crypto.subtle.importKey('spki', publicKey, algorithm, true, ['verify']) + console.log('Public Key:', publicKey) - const { x, y } = await crypto.subtle.exportKey('jwk', key) + try { + let publicKeyUint8Array: Uint8Array - const isValidCoordinates = !!x && !!y + if (typeof publicKey === 'string') { + console.log('Public Key is Base64') + publicKeyUint8Array = decodeBase64(publicKey) + } else if (publicKey instanceof ArrayBuffer) { + console.log('Public Key is ArrayBuffer') + publicKeyUint8Array = new Uint8Array(publicKey) + } else { + throw new Error('Unsupported public key format.') + } - if (!isValidCoordinates) { - throw new Error('Failed to generate passkey Coordinates. crypto.subtle.exportKey() failed') - } + console.log('Decoded Public Key Uint8Array:', publicKeyUint8Array) - return { - x: '0x' + Buffer.from(x, 'base64').toString('hex'), - y: '0x' + Buffer.from(y, 'base64').toString('hex') + if (publicKeyUint8Array.length === 0) { + throw new Error('Decoded public key is empty.') + } + + // Parse the DER-encoded public key using the ASN.1 schema + const decodedKey = AsnParser.parse(publicKeyUint8Array.buffer, ECPublicKey) + + // Extract the actual public key bytes + const keyData = new Uint8Array(decodedKey.publicKey) + + // Parse the public key bytes into a point on the curve + const point = p256.ProjectivePoint.fromHex(keyData) + + console.log('Elliptic Curve Point:', point) + + // Extract x and y coordinates + const x = point.x.toString(16).padStart(64, '0') + const y = point.y.toString(16).padStart(64, '0') + + return { + x: '0x' + x, + y: '0x' + y + } + } catch (error) { + console.error('Error decoding public key:', error) + throw error } } diff --git a/packages/protocol-kit/tests/e2e/utils/passkeys.ts b/packages/protocol-kit/tests/e2e/utils/passkeys.ts index 2053af959..676524a73 100644 --- a/packages/protocol-kit/tests/e2e/utils/passkeys.ts +++ b/packages/protocol-kit/tests/e2e/utils/passkeys.ts @@ -1,4 +1,4 @@ -import { PasskeyArgType, PasskeyClient, extractPasskeyCoordinates } from '@safe-global/protocol-kit' +import { PasskeyArgType, PasskeyClient } from '@safe-global/protocol-kit' import { WebAuthnCredentials } from './webauthnShim' import { WalletClient, keccak256, toBytes, Transport, Chain, Account } from 'viem' import { asHex } from '@safe-global/protocol-kit/utils/types' diff --git a/packages/sdk-starter-kit/package.json b/packages/sdk-starter-kit/package.json index e9443b1b5..7e1e72aac 100644 --- a/packages/sdk-starter-kit/package.json +++ b/packages/sdk-starter-kit/package.json @@ -37,7 +37,7 @@ }, "dependencies": { "@safe-global/api-kit": "^2.5.4", - "@safe-global/protocol-kit": "^5.0.4", + "@safe-global/protocol-kit": "file:.yalc/@safe-global/protocol-kit", "@safe-global/relay-kit": "^3.2.4", "@safe-global/types-kit": "^1.0.0", "viem": "^2.21.8" diff --git a/yarn.lock b/yarn.lock index 904bf6e2a..b8212855c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1136,6 +1136,13 @@ dependencies: "@noble/hashes" "1.4.0" +"@noble/curves@^1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.6.0.tgz#be5296ebcd5a1730fccea4786d420f87abfeb40b" + integrity sha512-TlaHRXDehJuRNR9TfZDNQ45mMEd5dwUwmicsafcIX4SsNiqnCHKjE/1alYPd/lDRVhxdhUAlv8uEhMCI5zjIJQ== + dependencies: + "@noble/hashes" "1.5.0" + "@noble/curves@~1.4.0": version "1.4.2" resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.4.2.tgz#40309198c76ed71bc6dbf7ba24e81ceb4d0d1fe9" @@ -1168,7 +1175,7 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426" integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg== -"@noble/hashes@^1.3.3", "@noble/hashes@^1.4.0", "@noble/hashes@~1.5.0": +"@noble/hashes@1.5.0", "@noble/hashes@^1.3.3", "@noble/hashes@^1.4.0", "@noble/hashes@~1.5.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.5.0.tgz#abadc5ca20332db2b1b2aa3e496e9af1213570b0" integrity sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA== @@ -1715,6 +1722,15 @@ resolved "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-2.5.1.tgz" integrity sha512-qIy6tLx8rtybEsIOAlrM4J/85s2q2nPkDqj/Rx46VakBZ0LwtFhXIVub96LXHczQX0vaqmAueDqNPXtbSXSaYQ== +"@peculiar/asn1-schema@^2.3.13": + version "2.3.13" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-schema/-/asn1-schema-2.3.13.tgz#ec8509cdcbc0da3abe73fd7e690556b57a61b8f4" + integrity sha512-3Xq3a01WkHRZL8X04Zsfg//mGaA21xlL4tlVn4v2xGT0JStiztATRkMwa5b+f/HXmY2smsiLXYK46Gwgzvfg3g== + dependencies: + asn1js "^3.0.5" + pvtsutils "^1.3.5" + tslib "^2.6.2" + "@pkgjs/parseargs@^0.11.0": version "0.11.0" resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" @@ -1725,6 +1741,19 @@ resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.1.tgz#1ec17e2edbec25c8306d424ecfbf13c7de1aaa31" integrity sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA== +"@safe-global/protocol-kit@file:packages/sdk-starter-kit/.yalc/@safe-global/protocol-kit": + version "5.0.4" + dependencies: + "@noble/curves" "^1.6.0" + "@noble/hashes" "^1.3.3" + "@peculiar/asn1-schema" "^2.3.13" + "@safe-global/safe-deployments" "^1.37.14" + "@safe-global/safe-modules-deployments" "^2.2.4" + "@safe-global/types-kit" "^1.0.0" + abitype "^1.0.2" + semver "^7.6.3" + viem "^2.21.8" + "@safe-global/safe-contracts-v1.4.1@npm:@safe-global/safe-contracts@1.4.1": version "1.4.1" resolved "https://registry.npmjs.org/@safe-global/safe-contracts/-/safe-contracts-1.4.1.tgz" @@ -2544,6 +2573,15 @@ arrify@^2.0.1: resolved "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz" integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== +asn1js@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/asn1js/-/asn1js-3.0.5.tgz#5ea36820443dbefb51cc7f88a2ebb5b462114f38" + integrity sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ== + dependencies: + pvtsutils "^1.3.2" + pvutils "^1.1.3" + tslib "^2.4.0" + assertion-error@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz" @@ -7263,6 +7301,18 @@ pure-rand@^6.0.0: resolved "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.1.tgz" integrity sha512-t+x1zEHDjBwkDGY5v5ApnZ/utcd4XYDiJsaQQoptTXgUXX95sDg1elCdJghzicm7n2mbCBJ3uYWr6M22SO19rg== +pvtsutils@^1.3.2, pvtsutils@^1.3.5: + version "1.3.5" + resolved "https://registry.yarnpkg.com/pvtsutils/-/pvtsutils-1.3.5.tgz#b8705b437b7b134cd7fd858f025a23456f1ce910" + integrity sha512-ARvb14YB9Nm2Xi6nBq1ZX6dAM0FsJnuk+31aUp4TrcZEdKUlSqOqsxJHUPJDNE3qiIp+iUPEIeR6Je/tgV7zsA== + dependencies: + tslib "^2.6.1" + +pvutils@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/pvutils/-/pvutils-1.1.3.tgz#f35fc1d27e7cd3dfbd39c0826d173e806a03f5a3" + integrity sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ== + qs@^6.9.4: version "6.11.1" resolved "https://registry.npmjs.org/qs/-/qs-6.11.1.tgz" @@ -8220,6 +8270,11 @@ tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4.0, tslib@^2.6.2: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== +tslib@^2.6.1: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + tsort@0.0.1: version "0.0.1" resolved "https://registry.npmjs.org/tsort/-/tsort-0.0.1.tgz"