diff --git a/packages/crypto/README.md b/packages/crypto/README.md index a48171a09..fb2a3c25b 100644 --- a/packages/crypto/README.md +++ b/packages/crypto/README.md @@ -1,10 +1,6 @@ # @turnkey/crypto -This package consolidates some common cryptographic utilities used across our applications, particularly primitives related to keys, encryption, and decryption in a pure JS implementation. For react-native you will need to polyfill our random byte generation by importing react-native-get-random-values: https://www.npmjs.com/package/react-native-get-random-values - -``` -import 'react-native-get-random-values' -``` +This package consolidates some common cryptographic utilities used across our applications, particularly primitives related to keys, encryption, and decryption in a pure JS implementation. Example usage (Hpke E2E): diff --git a/packages/crypto/jest.config.js b/packages/crypto/jest.config.js index b316684a3..7e10eb211 100644 --- a/packages/crypto/jest.config.js +++ b/packages/crypto/jest.config.js @@ -9,7 +9,6 @@ const config = { "/src/__tests__/shared.ts", ], testTimeout: 30 * 1000, // For Github CI machines. Locally tests are quite fast. - setupFiles: ["./setup.js"], }; module.exports = config; diff --git a/packages/crypto/package.json b/packages/crypto/package.json index b033e3cd9..1cad282a5 100644 --- a/packages/crypto/package.json +++ b/packages/crypto/package.json @@ -49,7 +49,6 @@ "bs58": "^5.0.0" }, "devDependencies": { - "crypto": "1.0.1", "jest": "29.7.0", "@turnkey/encoding": "workspace:*", "@turnkey/http": "workspace:*", diff --git a/packages/crypto/setup.js b/packages/crypto/setup.js deleted file mode 100644 index 9dcb11924..000000000 --- a/packages/crypto/setup.js +++ /dev/null @@ -1,4 +0,0 @@ -// setup.js -if (typeof crypto === "undefined") { - global.crypto = require("crypto"); -} diff --git a/packages/crypto/src/__tests__/crypto-test.ts b/packages/crypto/src/__tests__/crypto-test.ts index b5b29b533..79739819f 100644 --- a/packages/crypto/src/__tests__/crypto-test.ts +++ b/packages/crypto/src/__tests__/crypto-test.ts @@ -11,6 +11,7 @@ import { compressRawPublicKey, hpkeDecrypt, hpkeEncrypt, + decryptExportBundle, hpkeAuthEncrypt, formatHpkeBuf, verifyStampSignature, @@ -103,6 +104,50 @@ describe("HPKE Standard Encryption and Decryption", () => { }); }); +describe("decryptExportBundle Tests", () => { + const exportBundle = ` + { + "version": "v1.0.0", + "data": "7b22656e6361707065645075626c6963223a2230343434313065633837653566653266666461313561313866613337376132316133633431633334373666383631333362343238306164373631303266343064356462326463353362343730303763636139336166666330613535316464353134333937643039373931636664393233306663613330343862313731663364363738222c2263697068657274657874223a22656662303538626633666634626534653232323330326266326636303738363062343237346232623031616339343536643362613638646135613235363236303030613839383262313465306261663061306465323966353434353461333739613362653664633364386339343938376131353638633764393566396663346239316265663232316165356562383432333361323833323131346431373962646664636631643066376164656231353766343131613439383430222c226f7267616e697a6174696f6e4964223a2266396133316336342d643630342d343265342d396265662d613737333039366166616437227d", + "dataSignature": "304502203a7dc258590a637e76f6be6ed1a2080eed5614175060b9073f5e36592bdaf610022100ab9955b603df6cf45408067f652da48551652451b91967bf37dd094d13a7bdd4", + "enclaveQuorumPublic": "04cf288fe433cc4e1aa0ce1632feac4ea26bf2f5a09dcfe5a42c398e06898710330f0572882f4dbdf0f5304b8fc8703acd69adca9a4bbf7f5d00d20a5e364b2569" + } + `; + const privateKey = + "ffc6090f14bcf260e5dfe63f45412e60a477bb905956d7cc90195b71c2a544b3"; + const organizationId = "f9a31c64-d604-42e4-9bef-a773096afad7"; + + test("decryptExportBundle successfully decrypts a valid bundle - mnemonic", async () => { + const expectedMnemonic = + "leaf lady until indicate praise final route toast cake minimum insect unknown"; + + const result = await decryptExportBundle({ + exportBundle, + embeddedKey: privateKey, + organizationId, + keyFormat: "HEXADECIMAL", + returnMnemonic: true, + }); + + expect(result).toEqual(expectedMnemonic); + }); + + test("decryptExportBundle successfully decrypts a valid bundle - non-mnemonic", async () => { + const expectedNonMnemonic = + "6c656166206c61647920756e74696c20696e646963617465207072616973652066696e616c20726f75746520746f6173742063616b65206d696e696d756d20696e7365637420756e6b6e6f776e"; + + const result = await decryptExportBundle({ + exportBundle, + embeddedKey: privateKey, + organizationId, + keyFormat: "HEXADECIMAL", + returnMnemonic: false, + }); + + expect(result).toEqual(expectedNonMnemonic); + }); +}); + describe("Turnkey Crypto Primitives", () => { test("getPublicKey - returns the correct public key", () => { const keyPair = generateP256KeyPair(); diff --git a/packages/crypto/src/crypto.ts b/packages/crypto/src/crypto.ts index 693f2063a..3170f4823 100644 --- a/packages/crypto/src/crypto.ts +++ b/packages/crypto/src/crypto.ts @@ -3,6 +3,7 @@ import { p256 } from "@noble/curves/p256"; import * as hkdf from "@noble/hashes/hkdf"; import { sha256 } from "@noble/hashes/sha256"; import { gcm } from "@noble/ciphers/aes"; +import { randomBytes } from "@noble/hashes/utils"; import { uint8ArrayToHexString, @@ -374,14 +375,6 @@ export const uncompressRawPublicKey = ( return uint8ArrayFromHexString(uncompressedHexString); }; -/** - * Generate a random Uint8Array of a specific length. Note that this ultimately depends on the crypto implementation. - */ -const randomBytes = (length: number): Uint8Array => { - const array = new Uint8Array(length); - return crypto.getRandomValues(array); -}; - /** * Build labeled Initial Key Material (IKM). * @@ -527,12 +520,12 @@ const bigIntToHex = (num: bigint, length: number): string => { }; /** - * Converts an ASN.1 DER-encoded ECDSA signature to the raw format that WebCrypto uses. + * Converts an ASN.1 DER-encoded ECDSA signature to the raw format used for verification. * * @param {string} derSignature - The DER-encoded signature. * @returns {Uint8Array} - The raw signature. */ -export const fromDerSignature = (derSignature: string) => { +export const fromDerSignature = (derSignature: string): Uint8Array => { const derSignatureBuf = uint8ArrayFromHexString(derSignature); // Check and skip the sequence tag (0x30) diff --git a/packages/crypto/src/turnkey.ts b/packages/crypto/src/turnkey.ts index bb8caeed0..48af068bb 100644 --- a/packages/crypto/src/turnkey.ts +++ b/packages/crypto/src/turnkey.ts @@ -18,7 +18,10 @@ import { uncompressRawPublicKey, } from "./crypto"; +import { p256 } from "@noble/curves/p256"; import { ed25519 } from "@noble/curves/ed25519"; +import type { ProjPointType } from "@noble/curves/abstract/weierstrass"; +import { sha256 } from "@noble/hashes/sha256"; interface DecryptExportBundleParams { exportBundle: string; @@ -193,20 +196,17 @@ export const verifyStampSignature = async ( signedData: string, ): Promise => { const publicKeyBuffer = uint8ArrayFromHexString(publicKey); - const loadedPublicKey = await loadPublicKey(publicKeyBuffer); + const loadedPublicKey = loadPublicKey(publicKeyBuffer); if (!loadedPublicKey) { throw new Error("failed to load public key"); } - // The ECDSA signature is ASN.1 DER encoded but WebCrypto uses raw format + // Convert the ASN.1 DER-encoded signature for verification const publicSignatureBuf = fromDerSignature(signature); - const signedDataBuf = Buffer.from(signedData); - return await crypto.subtle.verify( - { name: "ECDSA", hash: { name: "SHA-256" } }, - loadedPublicKey, - publicSignatureBuf, - signedDataBuf, - ); + const signedDataBuf = new TextEncoder().encode(signedData); + const hashedData = sha256(signedDataBuf); + + return p256.verify(publicSignatureBuf, hashedData, loadedPublicKey.toHex()); }; /** @@ -237,39 +237,28 @@ const verifyEnclaveSignature = async ( const encryptionQuorumPublicBuf = new Uint8Array( uint8ArrayFromHexString(enclaveQuorumPublic), ); - const quorumKey = await loadPublicKey(encryptionQuorumPublicBuf); + const quorumKey = loadPublicKey(encryptionQuorumPublicBuf); if (!quorumKey) { throw new Error("failed to load quorum key"); } - // The ECDSA signature is ASN.1 DER encoded but WebCrypto uses raw format + // Convert the ASN.1 DER-encoded signature for verification const publicSignatureBuf = fromDerSignature(publicSignature); const signedDataBuf = uint8ArrayFromHexString(signedData); - return await crypto.subtle.verify( - { name: "ECDSA", hash: { name: "SHA-256" } }, - quorumKey, - publicSignatureBuf, - signedDataBuf, - ); + const hashedData = sha256(signedDataBuf); + + return p256.verify(publicSignatureBuf, hashedData, quorumKey.toHex()); }; /** * Loads an ECDSA public key from a raw format for signature verification. * - * @param {Uint8Array} publicKey - The raw public key bytes. - * @returns {Promise} - The imported ECDSA public key. + * @param {Uint8Array} publicKey - The raw P-256 public key bytes. + * @returns {ProjPointType} - The parsed ECDSA public key. + * @throws {Error} - If the public key is invalid. */ -const loadPublicKey = async (publicKey: Uint8Array): Promise => { - return await crypto.subtle.importKey( - "raw", - publicKey, - { - name: "ECDSA", - namedCurve: "P-256", - }, - true, - ["verify"], - ); +const loadPublicKey = (publicKey: Uint8Array): ProjPointType => { + return p256.ProjectivePoint.fromHex(uint8ArrayToHexString(publicKey)); }; /** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f6bb29534..f5b2ab0c2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1636,9 +1636,6 @@ importers: '@turnkey/sdk-server': specifier: workspace:* version: link:../sdk-server - crypto: - specifier: 1.0.1 - version: 1.0.1 jest: specifier: 29.7.0 version: 29.7.0(@types/node@18.18.2) @@ -15833,11 +15830,6 @@ packages: resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} dev: false - /crypto@1.0.1: - resolution: {integrity: sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==} - deprecated: This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in. - dev: true - /css-box-model@1.2.1: resolution: {integrity: sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==} dependencies: