Skip to content

Commit

Permalink
Merge pull request #500 from tkhq/moeO/crypto
Browse files Browse the repository at this point in the history
remove use of "crypto" library
  • Loading branch information
moeodeh3 authored Feb 10, 2025
2 parents 84d6d17 + 16d9ce7 commit eeb194b
Show file tree
Hide file tree
Showing 8 changed files with 68 additions and 59 deletions.
6 changes: 1 addition & 5 deletions packages/crypto/README.md
Original file line number Diff line number Diff line change
@@ -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):

Expand Down
1 change: 0 additions & 1 deletion packages/crypto/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ const config = {
"<rootDir>/src/__tests__/shared.ts",
],
testTimeout: 30 * 1000, // For Github CI machines. Locally tests are quite fast.
setupFiles: ["./setup.js"],
};

module.exports = config;
1 change: 0 additions & 1 deletion packages/crypto/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@
"bs58": "^5.0.0"
},
"devDependencies": {
"crypto": "1.0.1",
"jest": "29.7.0",
"@turnkey/encoding": "workspace:*",
"@turnkey/http": "workspace:*",
Expand Down
4 changes: 0 additions & 4 deletions packages/crypto/setup.js

This file was deleted.

45 changes: 45 additions & 0 deletions packages/crypto/src/__tests__/crypto-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
compressRawPublicKey,
hpkeDecrypt,
hpkeEncrypt,
decryptExportBundle,
hpkeAuthEncrypt,
formatHpkeBuf,
verifyStampSignature,
Expand Down Expand Up @@ -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();
Expand Down
13 changes: 3 additions & 10 deletions packages/crypto/src/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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).
*
Expand Down Expand Up @@ -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)
Expand Down
49 changes: 19 additions & 30 deletions packages/crypto/src/turnkey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -193,20 +196,17 @@ export const verifyStampSignature = async (
signedData: string,
): Promise<boolean> => {
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());
};

/**
Expand Down Expand Up @@ -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<CryptoKey>} - The imported ECDSA public key.
* @param {Uint8Array} publicKey - The raw P-256 public key bytes.
* @returns {ProjPointType<bigint>} - The parsed ECDSA public key.
* @throws {Error} - If the public key is invalid.
*/
const loadPublicKey = async (publicKey: Uint8Array): Promise<CryptoKey> => {
return await crypto.subtle.importKey(
"raw",
publicKey,
{
name: "ECDSA",
namedCurve: "P-256",
},
true,
["verify"],
);
const loadPublicKey = (publicKey: Uint8Array): ProjPointType<bigint> => {
return p256.ProjectivePoint.fromHex(uint8ArrayToHexString(publicKey));
};

/**
Expand Down
8 changes: 0 additions & 8 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit eeb194b

Please sign in to comment.