From 060c68b096288d5d7f1b6d954d0a50cb650c895c Mon Sep 17 00:00:00 2001 From: Dhruv Pareek Date: Tue, 9 Jun 2026 12:39:29 -0700 Subject: [PATCH] [grid] fix Global Account frontend auth sample --- .../embeddedWallet/AuthenticateAndSign.tsx | 32 ++++++------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/samples/frontend/src/steps/embeddedWallet/AuthenticateAndSign.tsx b/samples/frontend/src/steps/embeddedWallet/AuthenticateAndSign.tsx index c4780cc3..391cafff 100644 --- a/samples/frontend/src/steps/embeddedWallet/AuthenticateAndSign.tsx +++ b/samples/frontend/src/steps/embeddedWallet/AuthenticateAndSign.tsx @@ -35,8 +35,8 @@ const SANDBOX_WALLET_SIGNATURE = 'sandbox-valid-signature' // Grid returns encryptedSessionSigningKey sealed to clientPublicKey. // 5. HPKE-decrypt the session signing key with the client private key // (DHKEM-P256 / HKDF-SHA256 / AES-256-GCM, base58check wire format). -// 6. ECDSA-sign payloadToSign bytes with the session key, DER-encode, -// base64-encode, and pass to step 8 as the Grid-Wallet-Signature header. +// 6. Build a Turnkey API-key stamp over payloadToSign with the session key +// and pass the base64url stamp to step 8 as Grid-Wallet-Signature. // // Sandbox flow (this is what runs by default): // - Step 3 still triggers the real OS biometric prompt. @@ -82,7 +82,7 @@ export default function AuthenticateAndSign({ // signature or the sandbox magic value below. const assertion = (await navigator.credentials.get({ publicKey: { - challenge: base64urlToBytes(challenge.challenge), + challenge: new TextEncoder().encode(challenge.challenge), userVerification: 'required', timeout: 60_000, }, @@ -192,22 +192,16 @@ async function decryptSessionSigningKey( return new Uint8Array(plaintext) } -// Sign payloadToSign bytes verbatim (Grid hashes them on its side as part of -// signature verification). DER-encoded ECDSA signature, base64-encoded. +// Build the Turnkey API-key stamp expected by Grid-Wallet-Signature. function signPayload(sessionPrivateKey: Uint8Array, payloadToSign: string): string { const msg = new TextEncoder().encode(payloadToSign) const sig = p256.sign(msg, sessionPrivateKey, { format: 'der' }) - return bytesToBase64(sig as Uint8Array) -} - -function base64urlToBytes(s: string): ArrayBuffer { - const pad = '='.repeat((4 - (s.length % 4)) % 4) - const b64 = (s + pad).replace(/-/g, '+').replace(/_/g, '/') - const bin = atob(b64) - const buf = new ArrayBuffer(bin.length) - const view = new Uint8Array(buf) - for (let i = 0; i < bin.length; i++) view[i] = bin.charCodeAt(i) - return buf + const stamp = JSON.stringify({ + publicKey: bytesToHex(p256.getPublicKey(sessionPrivateKey, true)), + scheme: 'SIGNATURE_SCHEME_TK_API_P256', + signature: bytesToHex(sig as Uint8Array), + }) + return bytesToBase64url(new TextEncoder().encode(stamp)) } function bytesToBase64url(bytes: Uint8Array): string { @@ -216,12 +210,6 @@ function bytesToBase64url(bytes: Uint8Array): string { return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') } -function bytesToBase64(bytes: Uint8Array): string { - let bin = '' - for (const b of bytes) bin += String.fromCharCode(b) - return btoa(bin) -} - function bytesToHex(bytes: Uint8Array): string { return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('') }