diff --git a/Auth.tsx b/Auth.tsx index 942e152..cf0299e 100644 --- a/Auth.tsx +++ b/Auth.tsx @@ -1,23 +1,8 @@ -import Crypto from 'react-native-quick-crypto'; import React, { useState, useEffect } from 'react'; +import { Buffer } from '@craftzdog/react-native-buffer'; import { View, Text, TextInput, Button, StyleSheet } from 'react-native'; import { btoa, atob } from 'react-native-quick-base64' -import { - // importCredential, - // p256JWKPrivateToPublic, - compressRawPublicKey, - // uncompressRawPublicKey, - convertEcdsaIeee1363ToDer, - // hpkeDecrypt, - // generateTargetKey, - // base64urlEncode, - // base64urlDecode, - // base58checkDecode -} from '@turnkey/frame-utils'; -import AesGcmCrypto from 'react-native-aes-gcm-crypto'; import { p256 } from "@noble/curves/p256"; -import { TextEncoder, TextDecoder } from 'text-encoding'; -// import { hkdf } from '@noble/hashes/hkdf'; import * as hkdf from '@noble/hashes/hkdf' import { sha256 } from '@noble/hashes/sha256'; import { gcm } from '@noble/ciphers/aes'; @@ -35,24 +20,6 @@ const AuthScreen = () => { const SUITE_ID_1 = new Uint8Array([75,69,77,0,16]) const SUITE_ID_2 = new Uint8Array([72,80,75,69,0,16,0,1,0,2]) const HPKE_VERSION = new Uint8Array([72, 80, 75, 69, 45, 118, 49]); - - const LABEL_BASE_NONCE = new Uint8Array([ - 98, 97, 115, 101, 95, 110, 111, 110, 99, 101, - ]); - // b"exp" - const LABEL_EXP = new Uint8Array([101, 120, 112]); - // b"info_hash" - // deno-fmt-ignore - const LABEL_INFO_HASH = new Uint8Array([ - 105, 110, 102, 111, 95, 104, 97, 115, 104, - ]); - // b"key" - const LABEL_KEY = new Uint8Array([107, 101, 121]); - // b"psk_id_hash" - // deno-fmt-ignore - const LABEL_PSK_ID_HASH = new Uint8Array([ - 112, 115, 107, 95, 105, 100, 95, 104, 97, 115, 104, - ]); // b"secret" const LABEL_SECRET = new Uint8Array([115, 101, 99, 114, 101, 116]); // b"HPKE" @@ -65,21 +32,6 @@ const AuthScreen = () => { 114, 101, 116, ]); - /** - * Converts a `BigInt` into a base64url encoded string - * @param {BigInt} num - * @return {string} - */ - function bigIntToBase64Url (num:any) { - var hexString = num.toString(16); - // Add an extra 0 to the start of the string to get a valid hex string (even length) - // (e.g. 0x0123 instead of 0x123) - var hexString = hexString.padStart(Math.ceil(hexString.length/2)*2, 0) - var buffer = uint8arrayFromHexString(hexString); - return base64urlEncode(buffer) - } - - function buildLabeledIkm(label: Uint8Array, ikm: Uint8Array, suite_id: Uint8Array): Uint8Array { const combinedLength = HPKE_VERSION.length + suite_id.length + label.length + ikm.length; const ret = new Uint8Array(combinedLength); @@ -241,68 +193,22 @@ const base64urlDecode = (base64url: string): Uint8Array => { return bytes; }; -const generateTargetKey = async (): Promise => { - try { - // Generate a random private key - const randomBuf = Crypto.randomBytes(32); - const publicKey = await p256.getPublicKey(randomBuf, false); - const publicKeyHex = Buffer.from(publicKey).toString('hex'); - - if (publicKeyHex.startsWith('04')) { - const xCoordinate = base64urlEncode(Buffer.from(publicKeyHex.slice(2, 66), 'hex')); - const yCoordinate = base64urlEncode(Buffer.from(publicKeyHex.slice(66), 'hex')); - const dBase64url = base64urlEncode(randomBuf); - - // JWK format for EC private key - const privateKeyJWK = { - kty: 'EC', - crv: 'P-256', - x: xCoordinate, - y: yCoordinate, - d: dBase64url, - }; - - //hardcoded -// const privateKeyJWK = { -// kty: 'EC', -// crv: 'P-256', -// ext: true, -// x: "V5vpFxAkqni1ZNs20Hkln6af4civecIgl1XpU67CgaU", -// y: "Y84S3FCLuYlvKnCOmyiwrP_ol8qKK7BVRkS1lWdMv1U", -// d: "_kHJfupH1BuzLTIFKVLwLozqTTodhU_Zi7Jn1Rtl1dM", -// }; - - return privateKeyJWK; - } else { - throw new Error('Public key format is not uncompressed'); - } - } catch (error) { - console.error('Error generating key:', error); - throw error; - } -}; - -const p256JWKPrivateToPublic = async (privateJwk: any): Promise => { - const privateJwkCopy = { ...privateJwk }; - delete privateJwkCopy.d; - - try { - const publicKey = await Crypto.subtle.importKey( - 'jwk', - privateJwkCopy, - { name: 'ECDSA', namedCurve: 'P-256' }, - true, - ['verify'] - ); - const rawPublicKey = await Crypto.subtle.exportKey('raw', publicKey); - const resp = new Uint8Array(rawPublicKey) - return new Uint8Array(rawPublicKey); - } catch (error) { - console.error('Error converting private key to public key:', error); - throw error; +const randomBytes = (length: number): Uint8Array => { + const array = new Uint8Array(length); + for (let i = 0; i < length; i++) { + array[i] = Math.floor(Math.random() * 256); } + return array; }; + const generateTargetKey = async (): Promise<{ + privateKey: string; + publicKey: Uint8Array; + }> => { + const privateKey = randomBytes(32); + const publicKey = p256.getPublicKey(privateKey, false); + return { privateKey: base64urlEncode(privateKey), publicKey: publicKey }; + }; useEffect(() => { handleGenerateKey(); @@ -311,9 +217,8 @@ const p256JWKPrivateToPublic = async (privateJwk: any): Promise => { const handleGenerateKey = async () => { try { const key = await generateTargetKey(); - setEmbeddedKey(key); - const targetPubBuf = await p256JWKPrivateToPublic(key); - const targetPubHex = Buffer.from(targetPubBuf).toString('hex'); + setEmbeddedKey(key.privateKey); + const targetPubHex = Buffer.from(key.publicKey).toString('hex'); setPublicKey(targetPubHex); console.log(targetPubHex) } catch (error) { @@ -340,8 +245,8 @@ async function extractAndExpand(sharedSecret:any, ikm:any, info:any, len:any) { /** * Derive the dh using ECDH */ -const deriveDh = async (encappedKeyBuf:any, privateKeyJWK:any) => { - const dh = p256.getSharedSecret(base64urlDecode(privateKeyJWK.d), encappedKeyBuf) +const deriveDh = async (encappedKeyBuf:any, receiverPriv:any) => { + const dh = p256.getSharedSecret(base64urlDecode(receiverPriv), encappedKeyBuf) return dh.slice(1); }; @@ -368,16 +273,15 @@ const getKemContext = (encappedKeyBuf:any, publicKey:any) => { /** * HPKE Decrypt Function */ -const hpkeDecrypt = async ({ciphertextBuf, encappedKeyBuf, receiverPrivJwk}:any) => { +const hpkeDecrypt = async ({ciphertextBuf, encappedKeyBuf, receiverPriv}:any) => { try { let ikm let info - const receiverPubBuf = await p256JWKPrivateToPublic(receiverPrivJwk); + const receiverPubBuf = await p256.getPublicKey(base64urlDecode(receiverPriv), false); const aad = additionalAssociatedData(encappedKeyBuf, receiverPubBuf); const kemContext = getKemContext(encappedKeyBuf,publicKey) - // Step 1: Generate Shared Secret [DONE] - const dh = await deriveDh(encappedKeyBuf, receiverPrivJwk); + const dh = await deriveDh(encappedKeyBuf, receiverPriv); ikm = buildLabeledIkm(LABEL_EAE_PRK, dh, SUITE_ID_1) info = buildLabeledInfo(LABEL_SHARED_SECRET, kemContext, SUITE_ID_1, 32) const sharedSecret = await extractAndExpand(new Uint8Array([]), ikm, info, 32) @@ -421,16 +325,6 @@ function base64StringToBase64UrlEncodedString(input: string): string { // Uppercase o (O), uppercase i (I), lowercase L (l), and 0 aren't in the character set either. && credentialBundle.indexOf("O") === -1 && credentialBundle.indexOf("I") === -1 && credentialBundle.indexOf("l") === -1 && credentialBundle.indexOf("0") === -1 ) { - // If none of these characters are in the bundle we assume it's a base58check-encoded string - // This isn't perfect: there's a small chance that a base64url-encoded string doesn't have any of these characters by chance! - // But we accept this risk given this branching is only here to support our transition to base58check. - // I hear you'd like to quantify this risk? Let's do it. - // Assuming random bytes in our bundle and a bundle length of 33 (public key, compressed) + 48 (encrypted cred) = 81 bytes. - // The odds of a byte being in the overlap set between base58 and base64url is 58/64=0.90625. - // Which means the odds of a 81 bytes string being in the overlap character set for its entire length is... - // ... 0.90625^81 = 0.0003444209703 - // Are you convinced that this is good enough? I am :) - // var bundleBytes = await base58checkDecode(credentialBundle); var bundleBytes = bs58check.decode(credentialBundle) } else { var bundleBytes = base64urlDecode(credentialBundle); @@ -446,7 +340,7 @@ function base64StringToBase64UrlEncodedString(input: string): string { const decryptedData = await hpkeDecrypt({ ciphertextBuf: ciphertextBuf, encappedKeyBuf: encappedKeyBuf, - receiverPrivJwk: embeddedKey + receiverPriv: embeddedKey }); setDecryptedData(Buffer.from(decryptedData).toString('hex')); diff --git a/babel.config.js b/babel.config.js deleted file mode 100644 index d21b953..0000000 --- a/babel.config.js +++ /dev/null @@ -1,18 +0,0 @@ -module.exports = function(api) { - api.cache(true); - return { - presets: ['module:metro-react-native-babel-preset'], - plugins: [ - [ - 'module-resolver', - { - alias: { - 'crypto': 'react-native-quick-crypto', - 'stream': 'readable-stream', - 'buffer': '@craftzdog/react-native-buffer', - }, - }, - ], - ] - }; -}; diff --git a/package-lock.json b/package-lock.json index a378bfd..ed4ee57 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "passkeyapp", "version": "1.0.0", "dependencies": { + "@craftzdog/react-native-buffer": "^6.0.5", "@noble/ciphers": "^0.5.3", "@noble/curves": "^1.4.0", "@noble/hashes": "^1.4.0", @@ -18,22 +19,19 @@ "@turnkey/http": "^2.7.0", "@turnkey/react-native-passkey-stamper": "^0.1.0", "babel-plugin-module-resolver": "^5.0.2", - "bs58": "^5.0.0", + "base64-js": "^1.5.1", "bs58check": "^3.0.1", "crypto-js": "^4.2.0", "expo": "^50.0.17", "expo-status-bar": "~1.11.1", - "futoin-hkdf": "^1.5.3", "react": "18.2.0", "react-native": "0.73.6", "react-native-aes-crypto": "^3.0.2", "react-native-aes-gcm-crypto": "^0.2.2", "react-native-passkey": "^2.1.1", "react-native-quick-base64": "^2.1.2", - "react-native-quick-crypto": "0.7.0-rc.1", "react-native-safe-area-context": "4.8.2", - "react-native-screens": "~3.29.0", - "text-encoding": "^0.7.0" + "react-native-screens": "~3.29.0" }, "devDependencies": { "@babel/core": "^7.20.0", @@ -2784,6 +2782,14 @@ "safe-json-stringify": "~1" } }, + "node_modules/@expo/bunyan/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@expo/cli": { "version": "0.17.10", "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-0.17.10.tgz", @@ -4152,6 +4158,14 @@ "node": ">=12" } }, + "node_modules/@expo/rudder-sdk-node/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@expo/sdk-runtime-versions": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@expo/sdk-runtime-versions/-/sdk-runtime-versions-1.0.0.tgz", @@ -13405,14 +13419,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/futoin-hkdf": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/futoin-hkdf/-/futoin-hkdf-1.5.3.tgz", - "integrity": "sha512-SewY5KdMpaoCeh7jachEWFsh1nNlaDjNHZXWqL5IGwtpEYHTgkr2+AMCgNwKWkcc0wpSYrZfR7he4WdmHFtDxQ==", - "engines": { - "node": ">=8" - } - }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -20526,12 +20532,6 @@ "node": "*" } }, - "node_modules/text-encoding": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.7.0.tgz", - "integrity": "sha512-oJQ3f1hrOnbRLOcwKz0Liq2IcrvDeZRHXhd9RgLrsT+DjWY/nty1Hi7v3dtkaEYbPYe0mUoOfzRrMwfXXwgPUA==", - "deprecated": "no longer maintained" - }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -21043,14 +21043,6 @@ "node": ">= 0.4.0" } }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/v8-to-istanbul": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", diff --git a/package.json b/package.json index e387156..e2f44b4 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "web": "expo start --web" }, "dependencies": { + "@craftzdog/react-native-buffer": "^6.0.5", "@noble/ciphers": "^0.5.3", "@noble/curves": "^1.4.0", "@noble/hashes": "^1.4.0", @@ -19,22 +20,19 @@ "@turnkey/http": "^2.7.0", "@turnkey/react-native-passkey-stamper": "^0.1.0", "babel-plugin-module-resolver": "^5.0.2", - "bs58": "^5.0.0", + "base64-js": "^1.5.1", "bs58check": "^3.0.1", "crypto-js": "^4.2.0", "expo": "^50.0.17", "expo-status-bar": "~1.11.1", - "futoin-hkdf": "^1.5.3", "react": "18.2.0", "react-native": "0.73.6", "react-native-aes-crypto": "^3.0.2", "react-native-aes-gcm-crypto": "^0.2.2", "react-native-passkey": "^2.1.1", "react-native-quick-base64": "^2.1.2", - "react-native-quick-crypto": "0.7.0-rc.1", "react-native-safe-area-context": "4.8.2", - "react-native-screens": "~3.29.0", - "text-encoding": "^0.7.0" + "react-native-screens": "~3.29.0" }, "devDependencies": { "@babel/core": "^7.20.0",