Skip to content

Commit

Permalink
Add web working version
Browse files Browse the repository at this point in the history
  • Loading branch information
yagopv committed Nov 14, 2024
1 parent c6693cd commit 7cd103f
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 36 deletions.
2 changes: 2 additions & 0 deletions packages/protocol-kit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
12 changes: 10 additions & 2 deletions packages/protocol-kit/src/Safe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ import {
SigningMethodType,
SwapOwnerTxParams,
SafeModulesPaginated,
RemovePasskeyOwnerTxParams
RemovePasskeyOwnerTxParams,
PasskeyCoordinates
} from './types'
import {
EthSafeSignature,
Expand All @@ -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'
Expand Down Expand Up @@ -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
2 changes: 0 additions & 2 deletions packages/protocol-kit/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ import {
estimateTxGas,
estimateSafeTxGas,
estimateSafeDeploymentGas,
extractPasskeyCoordinates,
extractPasskeyData,
validateEthereumAddress,
validateEip3770Address
Expand Down Expand Up @@ -74,7 +73,6 @@ export {
estimateSafeTxGas,
estimateSafeDeploymentGas,
extractPasskeyData,
extractPasskeyCoordinates,
ContractManager,
CreateCallBaseContract,
createERC20TokenTransferTransaction,
Expand Down
111 changes: 82 additions & 29 deletions packages/protocol-kit/src/utils/passkeys/extractPasskeyData.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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 {
Expand All @@ -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 <https://webauthn.guide/#registration>.
*
* @param {ArrayBuffer} publicKey - The public key of the passkey from which coordinates will be extracted.
* @returns {Promise<PasskeyCoordinates>} 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<AuthenticatorAttestationResponse, 'attestationObject'>} 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<PasskeyCoordinates> {
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
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/protocol-kit/tests/e2e/utils/passkeys.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
2 changes: 1 addition & 1 deletion packages/sdk-starter-kit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
57 changes: 56 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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==
Expand Down Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down

0 comments on commit 7cd103f

Please sign in to comment.