Skip to content

Commit

Permalink
Merge pull request #153 from hjornigur/main
Browse files Browse the repository at this point in the history
Passkey plugin - Remove redundant calls to passkey-server during message signing
  • Loading branch information
adnpark authored Jun 25, 2024
2 parents 2e69bb2 + 6c98543 commit cdaec0e
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 80 deletions.
76 changes: 22 additions & 54 deletions plugins/passkey/toPasskeyValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@ import { getValidatorAddress } from "./index.js"
import type { WebAuthnKey } from "./toWebAuthnKey.js"
import {
b64ToBytes,
base64FromUint8Array,
deserializePasskeyValidatorData,
findQuoteIndices,
hexStringToUint8Array,
isRIP7212SupportedNetwork,
parseAndNormalizeSig,
serializePasskeyValidatorData,
Expand All @@ -39,8 +41,8 @@ import {

const signMessageUsingWebAuthn = async (
message: SignableMessage,
passkeyServerUrl: string,
chainId: number
chainId: number,
allowCredentials?: PublicKeyCredentialRequestOptionsJSON["allowCredentials"]
) => {
let messageContent: string
if (typeof message === "string") {
Expand All @@ -61,27 +63,15 @@ const signMessageUsingWebAuthn = async (
? messageContent.slice(2)
: messageContent

if (window.sessionStorage === undefined) {
throw new Error("sessionStorage is not available")
}
const userId = sessionStorage.getItem("userId")

// initiate signing
const signInitiateResponse = await fetch(
`${passkeyServerUrl}/sign-initiate`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ data: formattedMessage, userId }),
credentials: "include"
}
const challenge = base64FromUint8Array(
hexStringToUint8Array(formattedMessage),
true
)
const signInitiateResult = await signInitiateResponse.json()

// prepare assertion options
const assertionOptions: PublicKeyCredentialRequestOptionsJSON = {
challenge: signInitiateResult.challenge,
allowCredentials: signInitiateResult.allowCredentials,
challenge,
allowCredentials,
userVerification: "required"
}

Expand All @@ -90,22 +80,8 @@ const signMessageUsingWebAuthn = async (
const { startAuthentication } = await import("@simplewebauthn/browser")
const cred = await startAuthentication(assertionOptions)

// verify signature from server
const verifyResponse = await fetch(`${passkeyServerUrl}/sign-verify`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ cred, userId }),
credentials: "include"
})

const verifyResult = await verifyResponse.json()

if (!verifyResult.success) {
throw new Error("Signature not verified")
}

// get authenticator data
const authenticatorData = verifyResult.authenticatorData
const { authenticatorData } = cred.response
const authenticatorDataHex = uint8ArrayToHexString(
b64ToBytes(authenticatorData)
)
Expand All @@ -117,7 +93,7 @@ const signMessageUsingWebAuthn = async (
const { beforeType } = findQuoteIndices(clientDataJSON)

// get signature r,s
const signature = verifyResult.signature
const { signature } = cred.response
const signatureHex = uint8ArrayToHexString(b64ToBytes(signature))
const { r, s } = parseAndNormalizeSig(signatureHex)

Expand Down Expand Up @@ -151,14 +127,12 @@ export async function toPasskeyValidator<
client: Client<TTransport, TChain, undefined>,
{
webAuthnKey,
passkeyServerUrl,
entryPoint: entryPointAddress,
kernelVersion,
validatorAddress: _validatorAddress,
credentials = "include"
}: {
webAuthnKey: WebAuthnKey
passkeyServerUrl: string
entryPoint: entryPoint
kernelVersion: GetKernelVersion<entryPoint>
validatorAddress?: Address
Expand All @@ -181,7 +155,9 @@ export async function toPasskeyValidator<
// note that this address will be overwritten by actual address
address: "0x0000000000000000000000000000000000000000",
async signMessage({ message }) {
return signMessageUsingWebAuthn(message, passkeyServerUrl, chainId)
return signMessageUsingWebAuthn(message, chainId, [
{ id: webAuthnKey.authenticatorId, type: "public-key" }
])
},
async signTransaction(_, __) {
throw new SignTransactionNotSupportedBySmartAccount()
Expand Down Expand Up @@ -297,19 +273,14 @@ export async function toPasskeyValidator<
},

getSerializedData() {
if (window.sessionStorage === undefined) {
throw new Error("sessionStorage is not available")
}
const userId = sessionStorage.getItem("userId")
return serializePasskeyValidatorData({
passkeyServerUrl,
credentials,
entryPoint: entryPointAddress,
validatorAddress,
pubKeyX: webAuthnKey.pubX,
pubKeyY: webAuthnKey.pubY,
authenticatorIdHash: webAuthnKey.authenticatorIdHash,
userId: userId ?? ""
authenticatorId: webAuthnKey.authenticatorId,
authenticatorIdHash: webAuthnKey.authenticatorIdHash
})
}
}
Expand All @@ -336,12 +307,12 @@ export async function deserializePasskeyValidator<
}
> {
const {
passkeyServerUrl,
credentials,
entryPoint,
validatorAddress,
pubKeyX,
pubKeyY,
authenticatorId,
authenticatorIdHash
} = deserializePasskeyValidatorData(serializedData)

Expand All @@ -353,7 +324,9 @@ export async function deserializePasskeyValidator<
// note that this address will be overwritten by actual address
address: "0x0000000000000000000000000000000000000000",
async signMessage({ message }) {
return signMessageUsingWebAuthn(message, passkeyServerUrl, chainId)
return signMessageUsingWebAuthn(message, chainId, [
{ id: authenticatorId, type: "public-key" }
])
},
async signTransaction(_, __) {
throw new SignTransactionNotSupportedBySmartAccount()
Expand Down Expand Up @@ -464,19 +437,14 @@ export async function deserializePasskeyValidator<
return false
},
getSerializedData() {
if (window.sessionStorage === undefined) {
throw new Error("sessionStorage is not available")
}
const userId = sessionStorage.getItem("userId")
return serializePasskeyValidatorData({
passkeyServerUrl,
credentials,
entryPoint,
validatorAddress,
pubKeyX,
pubKeyY,
authenticatorIdHash,
userId: userId ?? ""
authenticatorId,
authenticatorIdHash
})
}
}
Expand Down
34 changes: 14 additions & 20 deletions plugins/passkey/toWebAuthnKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export enum WebAuthnMode {
export type WebAuthnKey = {
pubX: bigint
pubY: bigint
authenticatorId: string
authenticatorIdHash: Hex
}

Expand All @@ -29,7 +30,7 @@ export const toWebAuthnKey = async ({
return webAuthnKey
}
let pubKey: string | undefined
let authenticatorIdHash: Hex
let authenticatorId: string | undefined
if (mode === WebAuthnMode.Login) {
// Get login options
const loginOptionsResponse = await fetch(
Expand All @@ -46,10 +47,7 @@ export const toWebAuthnKey = async ({
const { startAuthentication } = await import("@simplewebauthn/browser")
const loginCred = await startAuthentication(loginOptions)

// get authenticatorIdHash
authenticatorIdHash = keccak256(
uint8ArrayToHexString(b64ToBytes(loginCred.id))
)
authenticatorId = loginCred.id

// Verify authentication
const loginVerifyResponse = await fetch(
Expand All @@ -64,11 +62,6 @@ export const toWebAuthnKey = async ({

const loginVerifyResult = await loginVerifyResponse.json()

if (window.sessionStorage === undefined) {
throw new Error("sessionStorage is not available")
}
sessionStorage.setItem("userId", loginVerifyResult.userId)

if (!loginVerifyResult.verification.verified) {
throw new Error("Login not verified")
}
Expand All @@ -89,20 +82,11 @@ export const toWebAuthnKey = async ({
)
const registerOptions = await registerOptionsResponse.json()

// save userId to sessionStorage
if (window.sessionStorage === undefined) {
throw new Error("sessionStorage is not available")
}
sessionStorage.setItem("userId", registerOptions.userId)

// Start registration
const { startRegistration } = await import("@simplewebauthn/browser")
const registerCred = await startRegistration(registerOptions.options)

// get authenticatorIdHash
authenticatorIdHash = keccak256(
uint8ArrayToHexString(b64ToBytes(registerCred.id))
)
authenticatorId = registerCred.id

// Verify registration
const registerVerifyResponse = await fetch(
Expand Down Expand Up @@ -133,6 +117,15 @@ export const toWebAuthnKey = async ({
if (!pubKey) {
throw new Error("No public key returned from registration credential")
}
if (!authenticatorId) {
throw new Error(
"No authenticator id returned from registration credential"
)
}

const authenticatorIdHash = keccak256(
uint8ArrayToHexString(b64ToBytes(authenticatorId))
)
const spkiDer = Buffer.from(pubKey, "base64")
const key = await crypto.subtle.importKey(
"spki",
Expand All @@ -156,6 +149,7 @@ export const toWebAuthnKey = async ({
return {
pubX: BigInt(`0x${pubKeyX}`),
pubY: BigInt(`0x${pubKeyY}`),
authenticatorId,
authenticatorIdHash
}
}
63 changes: 57 additions & 6 deletions plugins/passkey/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,20 @@ export const uint8ArrayToHexString = (array: Uint8Array): `0x${string}` => {
).join("")}` as `0x${string}`
}

export const hexStringToUint8Array = (hexString: string): Uint8Array => {
const formattedHexString = hexString.startsWith("0x")
? hexString.slice(2)
: hexString
const byteArray = new Uint8Array(formattedHexString.length / 2)
for (let i = 0; i < formattedHexString.length; i += 2) {
byteArray[i / 2] = Number.parseInt(
formattedHexString.substring(i, i + 2),
16
)
}
return byteArray
}

export const b64ToBytes = (base64: string): Uint8Array => {
const paddedBase64 = base64
.replace(/-/g, "+")
Expand Down Expand Up @@ -54,14 +68,13 @@ export function parseAndNormalizeSig(derSig: Hex): { r: bigint; s: bigint } {
}

type PasskeyValidatorSerializedData = {
passkeyServerUrl: string
credentials: string
entryPoint: Hex
validatorAddress: Hex
pubKeyX: bigint
pubKeyY: bigint
authenticatorId: string
authenticatorIdHash: Hex
userId: string
}

export const serializePasskeyValidatorData = (
Expand All @@ -85,10 +98,6 @@ export const deserializePasskeyValidatorData = (params: string) => {
const uint8Array = base64ToBytes(params)
const jsonString = new TextDecoder().decode(uint8Array)
const parsed = JSON.parse(jsonString) as PasskeyValidatorSerializedData
if (window.sessionStorage === undefined) {
throw new Error("sessionStorage is not available")
}
sessionStorage.setItem("userId", parsed.userId)
return parsed
}

Expand All @@ -101,3 +110,45 @@ function bytesToBase64(bytes: Uint8Array) {
const binString = Array.from(bytes, (x) => String.fromCodePoint(x)).join("")
return btoa(binString)
}

/**
* Convenience function for creating a base64 encoded string from an ArrayBuffer instance
* Copied from @hexagon/base64 package (base64.fromArrayBuffer)
* @public
*
* @param {Uint8Array} uint8Arr - Uint8Array to be encoded
* @param {boolean} [urlMode] - If set to true, URL mode string will be returned
* @returns {string} - Base64 representation of data
*/
export const base64FromUint8Array = (
uint8Arr: Uint8Array,
urlMode: boolean
): string => {
const // Regular base64 characters
chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
const // Base64url characters
charsUrl =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"

let result = ""

const len = uint8Arr.length
const target = urlMode ? charsUrl : chars

for (let i = 0; i < len; i += 3) {
result += target[uint8Arr[i] >> 2]
result += target[((uint8Arr[i] & 3) << 4) | (uint8Arr[i + 1] >> 4)]
result += target[((uint8Arr[i + 1] & 15) << 2) | (uint8Arr[i + 2] >> 6)]
result += target[uint8Arr[i + 2] & 63]
}

const remainder = len % 3
if (remainder === 2) {
result = result.substring(0, result.length - 1) + (urlMode ? "" : "=")
} else if (remainder === 1) {
result = result.substring(0, result.length - 2) + (urlMode ? "" : "==")
}

return result
}

0 comments on commit cdaec0e

Please sign in to comment.