From 20f973ae556eae5600f2aad8fa7d502b8d4d0bc2 Mon Sep 17 00:00:00 2001 From: Omkar Shanbhag Date: Thu, 8 Aug 2024 18:08:43 -0400 Subject: [PATCH 01/19] pad sdk browser key import --- packages/encoding/src/index.ts | 7 +++++++ packages/sdk-browser/src/utils.ts | 25 ++++++++++++++++++++++--- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/packages/encoding/src/index.ts b/packages/encoding/src/index.ts index 2872834c2..e2bae3096 100644 --- a/packages/encoding/src/index.ts +++ b/packages/encoding/src/index.ts @@ -30,6 +30,13 @@ export const uint8ArrayFromHexString = (hexString: string): Uint8Array => { ); }; +export const uint8ArrayFromHexStringPadded = (hexString: string, length: number): Uint8Array => { + var buffer = uint8ArrayFromHexString(hexString); + var paddedBuffer = new Uint8Array(length); + paddedBuffer.set(buffer, length - buffer.length); + return paddedBuffer; +}; + // Pure JS implementation of btoa. This is adapted from the following: // https://github.com/jsdom/abab/blob/80874ae1fe1cde2e587bb6e51b6d7c9b42ca1d34/lib/btoa.js function btoa(s: string): string { diff --git a/packages/sdk-browser/src/utils.ts b/packages/sdk-browser/src/utils.ts index 9d7dc4e0c..ed1cd8f5f 100644 --- a/packages/sdk-browser/src/utils.ts +++ b/packages/sdk-browser/src/utils.ts @@ -1,4 +1,4 @@ -import { uint8ArrayFromHexString } from "@turnkey/encoding"; +import { uint8ArrayFromHexString, uint8ArrayFromHexStringPadded } from "@turnkey/encoding"; import { generateP256KeyPair, buildAdditionalAssociatedData, @@ -28,11 +28,30 @@ export const createEmbeddedAPIKey = async ( aead: AeadId.Aes256Gcm, }); + // Function to extract x and y components from a hex string public key + function getXYComponentsFromHexString(publicKeyHex: string) { + publicKeyHex = publicKeyHex.replace(/^0x/, ''); + if (!publicKeyHex.startsWith('04')) { + throw new Error("Public key is not in uncompressed format"); + } + const x = publicKeyHex.slice(2, 66); + const y = publicKeyHex.slice(66, 130); + return { x, y }; + } + // 3: import the targetPublicKey (i.e. passed in from the iframe) const targetKeyBytes = uint8ArrayFromHexString(targetPublicKey); + const { x, y } = getXYComponentsFromHexString(targetPublicKey); + const targetKey = await crypto.subtle.importKey( - "raw", - targetKeyBytes, + "jwk", + { + kty: "EC", + crv: "P-256", + x: base64UrlEncode(uint8ArrayFromHexStringPadded(x, 32)), + y: base64UrlEncode(uint8ArrayFromHexStringPadded(y, 32)), + ext: true + }, // this is what needs to be changed { name: "ECDH", namedCurve: "P-256", From 852822aeab294228288029f6bb19d5584007a4e0 Mon Sep 17 00:00:00 2001 From: Omkar Shanbhag Date: Thu, 8 Aug 2024 19:14:22 -0400 Subject: [PATCH 02/19] add length check to pad, and a test --- packages/encoding/src/__tests__/index-test.ts | 25 +++++++++++++++++++ packages/encoding/src/index.ts | 5 +++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/packages/encoding/src/__tests__/index-test.ts b/packages/encoding/src/__tests__/index-test.ts index 8732ffbde..e9a493d22 100644 --- a/packages/encoding/src/__tests__/index-test.ts +++ b/packages/encoding/src/__tests__/index-test.ts @@ -3,6 +3,7 @@ import { stringToBase64urlString, uint8ArrayFromHexString, uint8ArrayToHexString, + uint8ArrayFromHexStringPadded, base64StringToBase64UrlEncodedString, } from ".."; @@ -69,3 +70,27 @@ test("uint8ArrayFromHexString", async function () { ]); expect(uint8ArrayFromHexString(hexString)).toEqual(expectedUint8Array); // Hex string => Uint8Array }); + +// Test for uint8ArrayFromHexStringPadded +// Convert hex string to uint8 array and pads it with zeroes to a particular length if its less +test("uint8ArrayFromHexStringPadded", async function () { + + // TOO SHORT - test a hex string with less bytes than the "length" parameter provided + const hexString = + "5234d08dfa2c815f3097b8ba848a28172e85bec78886e8e201afccb166fc"; // length is 30 bytes, so must be padded with 2 0's at the beginning + const expectedUint8Array = new Uint8Array([ + 0, 0, 82, 52, 208, 141, 250, 44, 129, 95, 48, 151, 184, 186, 132, 138, 40, 23, 46, + 133, 190, 199, 136, 134, 232, 226, 1, 175, 204, 177, 102, 252, + ]); + expect(uint8ArrayFromHexStringPadded(hexString, 32)).toEqual(expectedUint8Array); // Hex string => Uint8Array + + + // TOO LONG - test a hex string with less bytes than the "length" parameter provided + const hexString2 = + "5234d08dfa2c815f3097b8ba848a28172e85bec78886e8e201afccb166fcfafbfcfd"; // length is 34 bytes, so no additional padding will be added + const expectedUint8Array2 = new Uint8Array([ + 0, 0, 82, 52, 208, 141, 250, 44, 129, 95, 48, 151, 184, 186, 132, 138, 40, 23, 46, + 133, 190, 199, 136, 134, 232, 226, 1, 175, 204, 177, 102, 252, 250, 251, 252, 253, + ]); + expect(uint8ArrayFromHexStringPadded(hexString, 32)).toEqual(expectedUint8Array); // Hex string => Uint8Array +}); diff --git a/packages/encoding/src/index.ts b/packages/encoding/src/index.ts index e2bae3096..3a1e78b5a 100644 --- a/packages/encoding/src/index.ts +++ b/packages/encoding/src/index.ts @@ -31,7 +31,10 @@ export const uint8ArrayFromHexString = (hexString: string): Uint8Array => { }; export const uint8ArrayFromHexStringPadded = (hexString: string, length: number): Uint8Array => { - var buffer = uint8ArrayFromHexString(hexString); + var buffer = uint8ArrayFromHexString(hexString); + if (buffer.length >= length) { + return buffer + } var paddedBuffer = new Uint8Array(length); paddedBuffer.set(buffer, length - buffer.length); return paddedBuffer; From e04b54e05a0db507c5486fa137da4d5b8fbb9397 Mon Sep 17 00:00:00 2001 From: Omkar Shanbhag Date: Fri, 9 Aug 2024 12:07:27 -0400 Subject: [PATCH 03/19] remove extra method and add optional parameter --- packages/encoding/src/__tests__/index-test.ts | 9 ++++----- packages/encoding/src/index.ts | 12 +++--------- packages/sdk-browser/src/utils.ts | 8 ++++---- 3 files changed, 11 insertions(+), 18 deletions(-) diff --git a/packages/encoding/src/__tests__/index-test.ts b/packages/encoding/src/__tests__/index-test.ts index e9a493d22..f41d6c865 100644 --- a/packages/encoding/src/__tests__/index-test.ts +++ b/packages/encoding/src/__tests__/index-test.ts @@ -3,7 +3,6 @@ import { stringToBase64urlString, uint8ArrayFromHexString, uint8ArrayToHexString, - uint8ArrayFromHexStringPadded, base64StringToBase64UrlEncodedString, } from ".."; @@ -71,9 +70,9 @@ test("uint8ArrayFromHexString", async function () { expect(uint8ArrayFromHexString(hexString)).toEqual(expectedUint8Array); // Hex string => Uint8Array }); -// Test for uint8ArrayFromHexStringPadded +// Test array padding for uint8ArrayFromHexString // Convert hex string to uint8 array and pads it with zeroes to a particular length if its less -test("uint8ArrayFromHexStringPadded", async function () { +test("uint8ArrayFromHexString Test padding", async function () { // TOO SHORT - test a hex string with less bytes than the "length" parameter provided const hexString = @@ -82,7 +81,7 @@ test("uint8ArrayFromHexStringPadded", async function () { 0, 0, 82, 52, 208, 141, 250, 44, 129, 95, 48, 151, 184, 186, 132, 138, 40, 23, 46, 133, 190, 199, 136, 134, 232, 226, 1, 175, 204, 177, 102, 252, ]); - expect(uint8ArrayFromHexStringPadded(hexString, 32)).toEqual(expectedUint8Array); // Hex string => Uint8Array + expect(uint8ArrayFromHexString(hexString, 32)).toEqual(expectedUint8Array); // Hex string => Uint8Array // TOO LONG - test a hex string with less bytes than the "length" parameter provided @@ -92,5 +91,5 @@ test("uint8ArrayFromHexStringPadded", async function () { 0, 0, 82, 52, 208, 141, 250, 44, 129, 95, 48, 151, 184, 186, 132, 138, 40, 23, 46, 133, 190, 199, 136, 134, 232, 226, 1, 175, 204, 177, 102, 252, 250, 251, 252, 253, ]); - expect(uint8ArrayFromHexStringPadded(hexString, 32)).toEqual(expectedUint8Array); // Hex string => Uint8Array + expect(uint8ArrayFromHexString(hexString, 32)).toEqual(expectedUint8Array); // Hex string => Uint8Array }); diff --git a/packages/encoding/src/index.ts b/packages/encoding/src/index.ts index 3a1e78b5a..06114dbbe 100644 --- a/packages/encoding/src/index.ts +++ b/packages/encoding/src/index.ts @@ -18,21 +18,15 @@ export function uint8ArrayToHexString(input: Uint8Array): string { ); } -export const uint8ArrayFromHexString = (hexString: string): Uint8Array => { +export const uint8ArrayFromHexString = (hexString: string, length?: number): Uint8Array => { const hexRegex = /^[0-9A-Fa-f]+$/; if (!hexString || hexString.length % 2 != 0 || !hexRegex.test(hexString)) { throw new Error( `cannot create uint8array from invalid hex string: "${hexString}"` ); } - return new Uint8Array( - hexString!.match(/../g)!.map((h: string) => parseInt(h, 16)) - ); -}; - -export const uint8ArrayFromHexStringPadded = (hexString: string, length: number): Uint8Array => { - var buffer = uint8ArrayFromHexString(hexString); - if (buffer.length >= length) { + var buffer = new Uint8Array(hexString!.match(/../g)!.map((h: string) => parseInt(h, 16))); + if (length === undefined || buffer.length >= length) { return buffer } var paddedBuffer = new Uint8Array(length); diff --git a/packages/sdk-browser/src/utils.ts b/packages/sdk-browser/src/utils.ts index ed1cd8f5f..83ab8f5bd 100644 --- a/packages/sdk-browser/src/utils.ts +++ b/packages/sdk-browser/src/utils.ts @@ -1,4 +1,4 @@ -import { uint8ArrayFromHexString, uint8ArrayFromHexStringPadded } from "@turnkey/encoding"; +import { uint8ArrayFromHexString } from "@turnkey/encoding"; import { generateP256KeyPair, buildAdditionalAssociatedData, @@ -48,10 +48,10 @@ export const createEmbeddedAPIKey = async ( { kty: "EC", crv: "P-256", - x: base64UrlEncode(uint8ArrayFromHexStringPadded(x, 32)), - y: base64UrlEncode(uint8ArrayFromHexStringPadded(y, 32)), + x: base64UrlEncode(uint8ArrayFromHexString(x, 32)), + y: base64UrlEncode(uint8ArrayFromHexString(y, 32)), ext: true - }, // this is what needs to be changed + }, { name: "ECDH", namedCurve: "P-256", From c427f0dadd5cecff2ad637e9edd387b61033ca60 Mon Sep 17 00:00:00 2001 From: Omkar Shanbhag Date: Mon, 12 Aug 2024 17:47:13 -0400 Subject: [PATCH 04/19] fix testing --- packages/encoding/src/__tests__/index-test.ts | 14 ++++++++++-- .../sdk-browser/src/__tests__/utils-test.ts | 10 +++++++++ packages/sdk-browser/src/utils.ts | 22 +++++++++---------- 3 files changed, 33 insertions(+), 13 deletions(-) create mode 100644 packages/sdk-browser/src/__tests__/utils-test.ts diff --git a/packages/encoding/src/__tests__/index-test.ts b/packages/encoding/src/__tests__/index-test.ts index f41d6c865..562ccd88e 100644 --- a/packages/encoding/src/__tests__/index-test.ts +++ b/packages/encoding/src/__tests__/index-test.ts @@ -88,8 +88,18 @@ test("uint8ArrayFromHexString Test padding", async function () { const hexString2 = "5234d08dfa2c815f3097b8ba848a28172e85bec78886e8e201afccb166fcfafbfcfd"; // length is 34 bytes, so no additional padding will be added const expectedUint8Array2 = new Uint8Array([ - 0, 0, 82, 52, 208, 141, 250, 44, 129, 95, 48, 151, 184, 186, 132, 138, 40, 23, 46, + 82, 52, 208, 141, 250, 44, 129, 95, 48, 151, 184, 186, 132, 138, 40, 23, 46, 133, 190, 199, 136, 134, 232, 226, 1, 175, 204, 177, 102, 252, 250, 251, 252, 253, ]); - expect(uint8ArrayFromHexString(hexString, 32)).toEqual(expectedUint8Array); // Hex string => Uint8Array + expect(uint8ArrayFromHexString(hexString2, 32)).toEqual(expectedUint8Array2); // Hex string => Uint8Array }); + +test("uint8ArrayFromHexString zeroes", async function () { + const hexString = + "0000d08dfa2c815f3097b8ba848a28172e85bec78886e8e201afccb166fc54c1"; + const expectedUint8Array = new Uint8Array([ + 82, 52, 208, 141, 250, 44, 129, 95, 48, 151, 184, 186, 132, 138, 40, 23, 46, + 133, 190, 199, 136, 134, 232, 226, 1, 175, 204, 177, 102, 252, 84, 193, + ]); + expect(uint8ArrayFromHexString(hexString)).toEqual(expectedUint8Array); // Hex string => Uint8Array +}); \ No newline at end of file diff --git a/packages/sdk-browser/src/__tests__/utils-test.ts b/packages/sdk-browser/src/__tests__/utils-test.ts new file mode 100644 index 000000000..a9f3d4022 --- /dev/null +++ b/packages/sdk-browser/src/__tests__/utils-test.ts @@ -0,0 +1,10 @@ +import { test, expect } from "@jest/globals"; +import { + createEmbeddedAPIKey, +} from "../utils"; + +// Test to see that createEmbeddedAPIKey succeeds with a valid uncompressed public key +test("createEmbeddedAPIKey", async function () { + const result = await createEmbeddedAPIKey("04413029cb9a5a4a0b087a9b8a060116d0d32bb22d14aebf7778215744811bb6ce40780d7bb9e2e068879f443e05b21b8fc0b62c9c811008064d988856077e35e7"); + expect(result).toBe({}); +}); \ No newline at end of file diff --git a/packages/sdk-browser/src/utils.ts b/packages/sdk-browser/src/utils.ts index 83ab8f5bd..b374e4ab1 100644 --- a/packages/sdk-browser/src/utils.ts +++ b/packages/sdk-browser/src/utils.ts @@ -28,17 +28,6 @@ export const createEmbeddedAPIKey = async ( aead: AeadId.Aes256Gcm, }); - // Function to extract x and y components from a hex string public key - function getXYComponentsFromHexString(publicKeyHex: string) { - publicKeyHex = publicKeyHex.replace(/^0x/, ''); - if (!publicKeyHex.startsWith('04')) { - throw new Error("Public key is not in uncompressed format"); - } - const x = publicKeyHex.slice(2, 66); - const y = publicKeyHex.slice(66, 130); - return { x, y }; - } - // 3: import the targetPublicKey (i.e. passed in from the iframe) const targetKeyBytes = uint8ArrayFromHexString(targetPublicKey); const { x, y } = getXYComponentsFromHexString(targetPublicKey); @@ -114,3 +103,14 @@ export const bytesToHex = (bytes: Uint8Array): string => { } return hex; }; + +// Function to extract x and y components from a hex string public key +function getXYComponentsFromHexString(publicKeyHex: string) { + publicKeyHex = publicKeyHex.replace(/^0x/, ''); + if (!publicKeyHex.startsWith('04')) { + throw new Error("Public key is not in uncompressed format"); + } + const x = publicKeyHex.slice(2, 66); + const y = publicKeyHex.slice(66, 130); + return { x, y }; +} \ No newline at end of file From e3187d795b6153bd265f5ff89daf4bf03edadec3 Mon Sep 17 00:00:00 2001 From: Omkar Shanbhag Date: Mon, 12 Aug 2024 17:56:21 -0400 Subject: [PATCH 05/19] remove unnecessary test --- packages/encoding/src/__tests__/index-test.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/packages/encoding/src/__tests__/index-test.ts b/packages/encoding/src/__tests__/index-test.ts index 562ccd88e..2cce01a3d 100644 --- a/packages/encoding/src/__tests__/index-test.ts +++ b/packages/encoding/src/__tests__/index-test.ts @@ -92,14 +92,4 @@ test("uint8ArrayFromHexString Test padding", async function () { 133, 190, 199, 136, 134, 232, 226, 1, 175, 204, 177, 102, 252, 250, 251, 252, 253, ]); expect(uint8ArrayFromHexString(hexString2, 32)).toEqual(expectedUint8Array2); // Hex string => Uint8Array -}); - -test("uint8ArrayFromHexString zeroes", async function () { - const hexString = - "0000d08dfa2c815f3097b8ba848a28172e85bec78886e8e201afccb166fc54c1"; - const expectedUint8Array = new Uint8Array([ - 82, 52, 208, 141, 250, 44, 129, 95, 48, 151, 184, 186, 132, 138, 40, 23, 46, - 133, 190, 199, 136, 134, 232, 226, 1, 175, 204, 177, 102, 252, 84, 193, - ]); - expect(uint8ArrayFromHexString(hexString)).toEqual(expectedUint8Array); // Hex string => Uint8Array }); \ No newline at end of file From 782f50b14cadf61d054f5481e5e75fffeffd5fd7 Mon Sep 17 00:00:00 2001 From: Omkar Shanbhag Date: Tue, 13 Aug 2024 14:42:10 -0400 Subject: [PATCH 06/19] move logic to point decode --- .../src/tink/elliptic_curves.ts | 81 ++++++++++++++----- .../sdk-browser/src/__tests__/utils-test.ts | 10 --- packages/sdk-browser/src/utils.ts | 22 +---- 3 files changed, 63 insertions(+), 50 deletions(-) delete mode 100644 packages/sdk-browser/src/__tests__/utils-test.ts diff --git a/packages/api-key-stamper/src/tink/elliptic_curves.ts b/packages/api-key-stamper/src/tink/elliptic_curves.ts index eb6bd9abb..02767a93d 100644 --- a/packages/api-key-stamper/src/tink/elliptic_curves.ts +++ b/packages/api-key-stamper/src/tink/elliptic_curves.ts @@ -128,34 +128,73 @@ function getY(x: bigint, lsb: boolean): bigint { } /** - * Decodes a public key in _compressed_ format. + * + * Given x and y coordinates of a JWK, checks whether these are valid points on + * the P-256 elliptic curve. + * + * P-256 only + * + * @param x x-coordinate + * @param y y-coordinate + * @return boolean validitiy + */ +function validateUncompressedXY(x: bigint, y: bigint): boolean { + const p = getModulus(); + const a = p - BigInt(3); + const b = getB(); + const rhs = ((x * x + a) * x + b) % p; + const lhs = (y ** BigInt(2)) % p + return lhs === rhs; +} + +/** + * Decodes a public key in _compressed_ OR _uncompressed_ format. * * P-256 only */ export function pointDecode(point: Uint8Array): JsonWebKey { + const fieldSize = fieldSizeInBytes(); - - if (point.length !== 1 + fieldSize) { - throw new Error("compressed point has wrong length"); - } - if (point[0] !== 2 && point[0] !== 3) { - throw new Error("invalid format"); + const compressedLength = fieldSize + 1; + const uncompressedLength = (2 * fieldSize) + 1; + if (point.length !== compressedLength && point.length !== uncompressedLength) { + throw new Error("Invalid length: point is not in compressed or uncompressed format"); } - const lsb = point[0] === 3; // point[0] must be 2 (false) or 3 (true). - const x = byteArrayToInteger(point.subarray(1, point.length)); - const p = getModulus(); - if (x < BigInt(0) || x >= p) { - throw new Error("x is out of range"); + // Decodes point if its length and first bit match the compressed format + if ((point[0] === 2 || point[0] === 3) && point.length == compressedLength) { + const lsb = point[0] === 3; // point[0] must be 2 (false) or 3 (true). + const x = byteArrayToInteger(point.subarray(1, point.length)); + const p = getModulus(); + if (x < BigInt(0) || x >= p) { + throw new Error("x is out of range"); + } + const y = getY(x, lsb); + const result: JsonWebKey = { + kty: "EC", + crv: "P-256", + x: Bytes.toBase64(integerToByteArray(x), /* websafe */ true), + y: Bytes.toBase64(integerToByteArray(y), /* websafe */ true), + ext: true, + }; + return result; + // Decodes point if its length and first bit match the uncompressed format + } else if (point[0] === 4 && point.length == uncompressedLength) { + const x = byteArrayToInteger(point.subarray(1, fieldSize + 1)) + const y = byteArrayToInteger(point.subarray(fieldSize + 1, (2 * fieldSize) + 1)) + const p = getModulus(); + if (x < BigInt(0) || x >= p || y < BigInt(0) || y >= p || !validateUncompressedXY(x, y)) { + throw new Error("invalid uncompressed x and y coordinates"); + } + const result: JsonWebKey = { + kty: "EC", + crv: "P-256", + x: Bytes.toBase64(integerToByteArray(x), /* websafe */ true), + y: Bytes.toBase64(integerToByteArray(y), /* websafe */ true), + ext: true, + }; + return result } - const y = getY(x, lsb); - const result: JsonWebKey = { - kty: "EC", - crv: "P-256", - x: Bytes.toBase64(integerToByteArray(x), /* websafe */ true), - y: Bytes.toBase64(integerToByteArray(y), /* websafe */ true), - ext: true, - }; - return result; + throw new Error("invalid format"); } /** diff --git a/packages/sdk-browser/src/__tests__/utils-test.ts b/packages/sdk-browser/src/__tests__/utils-test.ts deleted file mode 100644 index a9f3d4022..000000000 --- a/packages/sdk-browser/src/__tests__/utils-test.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { test, expect } from "@jest/globals"; -import { - createEmbeddedAPIKey, -} from "../utils"; - -// Test to see that createEmbeddedAPIKey succeeds with a valid uncompressed public key -test("createEmbeddedAPIKey", async function () { - const result = await createEmbeddedAPIKey("04413029cb9a5a4a0b087a9b8a060116d0d32bb22d14aebf7778215744811bb6ce40780d7bb9e2e068879f443e05b21b8fc0b62c9c811008064d988856077e35e7"); - expect(result).toBe({}); -}); \ No newline at end of file diff --git a/packages/sdk-browser/src/utils.ts b/packages/sdk-browser/src/utils.ts index b374e4ab1..6edc16ccd 100644 --- a/packages/sdk-browser/src/utils.ts +++ b/packages/sdk-browser/src/utils.ts @@ -10,6 +10,7 @@ import bs58check from "bs58check"; import { AeadId, CipherSuite, KdfId, KemId } from "hpke-js"; import type { EmbeddedAPIKey } from "./models"; +import { pointDecode } from "../../api-key-stamper/src/tink/elliptic_curves"; // createEmbeddedAPIKey creates an embedded API key encrypted to a target key (typically embedded within an iframe). // This returns a bundle that can be decrypted by that target key, as well as the public key of the newly created API key. @@ -30,17 +31,11 @@ export const createEmbeddedAPIKey = async ( // 3: import the targetPublicKey (i.e. passed in from the iframe) const targetKeyBytes = uint8ArrayFromHexString(targetPublicKey); - const { x, y } = getXYComponentsFromHexString(targetPublicKey); + const jwk = pointDecode(targetKeyBytes); const targetKey = await crypto.subtle.importKey( "jwk", - { - kty: "EC", - crv: "P-256", - x: base64UrlEncode(uint8ArrayFromHexString(x, 32)), - y: base64UrlEncode(uint8ArrayFromHexString(y, 32)), - ext: true - }, + jwk, { name: "ECDH", namedCurve: "P-256", @@ -103,14 +98,3 @@ export const bytesToHex = (bytes: Uint8Array): string => { } return hex; }; - -// Function to extract x and y components from a hex string public key -function getXYComponentsFromHexString(publicKeyHex: string) { - publicKeyHex = publicKeyHex.replace(/^0x/, ''); - if (!publicKeyHex.startsWith('04')) { - throw new Error("Public key is not in uncompressed format"); - } - const x = publicKeyHex.slice(2, 66); - const y = publicKeyHex.slice(66, 130); - return { x, y }; -} \ No newline at end of file From 4fa3a9916ff3b5cd85780f8edc66bc275e3c90e0 Mon Sep 17 00:00:00 2001 From: Omkar Shanbhag Date: Tue, 13 Aug 2024 15:05:31 -0400 Subject: [PATCH 07/19] fix imports --- packages/sdk-browser/src/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sdk-browser/src/utils.ts b/packages/sdk-browser/src/utils.ts index 6edc16ccd..0f282dc5d 100644 --- a/packages/sdk-browser/src/utils.ts +++ b/packages/sdk-browser/src/utils.ts @@ -10,7 +10,7 @@ import bs58check from "bs58check"; import { AeadId, CipherSuite, KdfId, KemId } from "hpke-js"; import type { EmbeddedAPIKey } from "./models"; -import { pointDecode } from "../../api-key-stamper/src/tink/elliptic_curves"; +import { pointDecode } from "@turnkey/api-key-stamper/src/tink/elliptic_curves"; // createEmbeddedAPIKey creates an embedded API key encrypted to a target key (typically embedded within an iframe). // This returns a bundle that can be decrypted by that target key, as well as the public key of the newly created API key. From 98cb8713efcd39e020e3dd7e1df349a72e26626a Mon Sep 17 00:00:00 2001 From: Omkar Shanbhag Date: Wed, 14 Aug 2024 19:19:50 -0400 Subject: [PATCH 08/19] add tests for point decode uncompressed --- .../src/__tests__/elliptic-curves-test.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 packages/api-key-stamper/src/__tests__/elliptic-curves-test.ts diff --git a/packages/api-key-stamper/src/__tests__/elliptic-curves-test.ts b/packages/api-key-stamper/src/__tests__/elliptic-curves-test.ts new file mode 100644 index 000000000..09485b0e9 --- /dev/null +++ b/packages/api-key-stamper/src/__tests__/elliptic-curves-test.ts @@ -0,0 +1,32 @@ +import { test, expect } from "@jest/globals"; +import { pointDecode } from "../tink/elliptic_curves"; +import { uint8ArrayFromHexString, uint8ArrayToHexString } from "@turnkey/encoding"; +import { base64urlToBuffer, Base64urlString } from "../../../http/src/webauthn-json/base64url" + +test("pointDecode -> uncompressed invalid", async function () { + // Invalid uncompressed key (the last byte has been changed) + const uncompPubKey = "0400d2eb47be2006c29db5fe9941dd686d19ddeea85a0328894f08091f6b5be9b366c4872345594c12a7f7c47c62dd8074542934820fce5ee0ddc55d6d1d8dd313" + expect(() => pointDecode(uint8ArrayFromHexString(uncompPubKey))).toThrow("invalid uncompressed x and y coordinates"); +}); + +test("pointDecode -> uncompressed valid", async function () { + // Valid uncompressed public key with 00 as the first bit (test against 'x' field of JWK getting truncated) + const uncompPubKey = "0400d2eb47be2006c29db5fe9941dd686d19ddeea85a0328894f08091f6b5be9b366c4872345594c12a7f7c47c62dd8074542934820fce5ee0ddc55d6d1d8dd312" + const jwk = pointDecode(uint8ArrayFromHexString(uncompPubKey)); + expect(jwk.x).toBeDefined(); + expect(jwk.y).toBeDefined(); + + // Convert x value to make sure it matches first half of uncompressed key WITHOUT truncating the first 0 bit + let xString: string = jwk.x !== undefined ? jwk.x: "_"; + let xBase64Url: Base64urlString = xString; + let xBytes = new Uint8Array(base64urlToBuffer(xBase64Url)); + let xHex = uint8ArrayToHexString(xBytes); + expect(xHex).toBe(uncompPubKey.substring(2, 66)) + + // Convert y value to make sure it's the same as second half of uncompressed key + let yString: string = jwk.y !== undefined ? jwk.y: "_"; + let yBase64Url: Base64urlString = yString; + let yBytes = new Uint8Array(base64urlToBuffer(yBase64Url)); + let yHex = uint8ArrayToHexString(yBytes); + expect(yHex).toBe(uncompPubKey.substring(66, 130)) +}); \ No newline at end of file From 40885794963fa0300cc16cd76e2aeb500a04b84d Mon Sep 17 00:00:00 2001 From: Omkar Shanbhag Date: Thu, 15 Aug 2024 11:43:01 -0400 Subject: [PATCH 09/19] test padding --- .../src/__tests__/elliptic-curves-test.ts | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/packages/api-key-stamper/src/__tests__/elliptic-curves-test.ts b/packages/api-key-stamper/src/__tests__/elliptic-curves-test.ts index 09485b0e9..6a814e132 100644 --- a/packages/api-key-stamper/src/__tests__/elliptic-curves-test.ts +++ b/packages/api-key-stamper/src/__tests__/elliptic-curves-test.ts @@ -1,7 +1,6 @@ import { test, expect } from "@jest/globals"; import { pointDecode } from "../tink/elliptic_curves"; import { uint8ArrayFromHexString, uint8ArrayToHexString } from "@turnkey/encoding"; -import { base64urlToBuffer, Base64urlString } from "../../../http/src/webauthn-json/base64url" test("pointDecode -> uncompressed invalid", async function () { // Invalid uncompressed key (the last byte has been changed) @@ -18,15 +17,34 @@ test("pointDecode -> uncompressed valid", async function () { // Convert x value to make sure it matches first half of uncompressed key WITHOUT truncating the first 0 bit let xString: string = jwk.x !== undefined ? jwk.x: "_"; - let xBase64Url: Base64urlString = xString; - let xBytes = new Uint8Array(base64urlToBuffer(xBase64Url)); + let xBytes = new Uint8Array(base64urlToBuffer(xString)); let xHex = uint8ArrayToHexString(xBytes); expect(xHex).toBe(uncompPubKey.substring(2, 66)) // Convert y value to make sure it's the same as second half of uncompressed key let yString: string = jwk.y !== undefined ? jwk.y: "_"; - let yBase64Url: Base64urlString = yString; - let yBytes = new Uint8Array(base64urlToBuffer(yBase64Url)); + let yBytes = new Uint8Array(base64urlToBuffer(yString)); let yHex = uint8ArrayToHexString(yBytes); expect(yHex).toBe(uncompPubKey.substring(66, 130)) -}); \ No newline at end of file +}); + +// Convert base64 url encoded string to an array -- used here to test that output pads correctly and doesn't get truncated +function base64urlToBuffer( + baseurl64String: string + ): ArrayBuffer { + // Base64url to Base64 + const padding = "==".slice(0, (4 - (baseurl64String.length % 4)) % 4); + const base64String = + baseurl64String.replace(/-/g, "+").replace(/_/g, "/") + padding; + + // Base64 to binary string + const str = atob(base64String); + + // Binary string to buffer + const buffer = new ArrayBuffer(str.length); + const byteView = new Uint8Array(buffer); + for (let i = 0; i < str.length; i++) { + byteView[i] = str.charCodeAt(i); + } + return buffer; +} \ No newline at end of file From be750faf97167de9117750ae5039cee20dd9604b Mon Sep 17 00:00:00 2001 From: Omkar Shanbhag Date: Thu, 15 Aug 2024 12:02:17 -0400 Subject: [PATCH 10/19] correct padding tests --- packages/encoding/src/__tests__/index-test.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/encoding/src/__tests__/index-test.ts b/packages/encoding/src/__tests__/index-test.ts index 5f769c14a..a190ff19b 100644 --- a/packages/encoding/src/__tests__/index-test.ts +++ b/packages/encoding/src/__tests__/index-test.ts @@ -125,12 +125,8 @@ test("uint8ArrayFromHexString Test padding", async function () { expect(uint8ArrayFromHexString(hexString, 32)).toEqual(expectedUint8Array); // Hex string => Uint8Array - // TOO LONG - test a hex string with less bytes than the "length" parameter provided + // TOO LONG - test a hex string with less bytes than the "length" parameter provided -- Should error const hexString2 = "5234d08dfa2c815f3097b8ba848a28172e85bec78886e8e201afccb166fcfafbfcfd"; // length is 34 bytes, so no additional padding will be added - const expectedUint8Array2 = new Uint8Array([ - 82, 52, 208, 141, 250, 44, 129, 95, 48, 151, 184, 186, 132, 138, 40, 23, 46, - 133, 190, 199, 136, 134, 232, 226, 1, 175, 204, 177, 102, 252, 250, 251, 252, 253, - ]); - expect(uint8ArrayFromHexString(hexString2, 32)).toEqual(expectedUint8Array2); // Hex string => Uint8Array + expect(() => uint8ArrayFromHexString(hexString2, 32)).toThrow("hex value cannot fit in a buffer of 32 byte(s)") }); \ No newline at end of file From af1986815a338499999a62ee0447cd6b89689f35 Mon Sep 17 00:00:00 2001 From: Omkar Shanbhag Date: Thu, 15 Aug 2024 13:21:00 -0400 Subject: [PATCH 11/19] prettier --- .../src/__tests__/elliptic-curves-test.ts | 81 ++++++++++--------- .../src/tink/elliptic_curves.ts | 32 +++++--- packages/encoding/src/__tests__/index-test.ts | 14 ++-- 3 files changed, 72 insertions(+), 55 deletions(-) diff --git a/packages/api-key-stamper/src/__tests__/elliptic-curves-test.ts b/packages/api-key-stamper/src/__tests__/elliptic-curves-test.ts index 6a814e132..b21d6e73e 100644 --- a/packages/api-key-stamper/src/__tests__/elliptic-curves-test.ts +++ b/packages/api-key-stamper/src/__tests__/elliptic-curves-test.ts @@ -1,50 +1,55 @@ import { test, expect } from "@jest/globals"; import { pointDecode } from "../tink/elliptic_curves"; -import { uint8ArrayFromHexString, uint8ArrayToHexString } from "@turnkey/encoding"; +import { + uint8ArrayFromHexString, + uint8ArrayToHexString, +} from "@turnkey/encoding"; test("pointDecode -> uncompressed invalid", async function () { - // Invalid uncompressed key (the last byte has been changed) - const uncompPubKey = "0400d2eb47be2006c29db5fe9941dd686d19ddeea85a0328894f08091f6b5be9b366c4872345594c12a7f7c47c62dd8074542934820fce5ee0ddc55d6d1d8dd313" - expect(() => pointDecode(uint8ArrayFromHexString(uncompPubKey))).toThrow("invalid uncompressed x and y coordinates"); + // Invalid uncompressed key (the last byte has been changed) + const uncompPubKey = + "0400d2eb47be2006c29db5fe9941dd686d19ddeea85a0328894f08091f6b5be9b366c4872345594c12a7f7c47c62dd8074542934820fce5ee0ddc55d6d1d8dd313"; + expect(() => pointDecode(uint8ArrayFromHexString(uncompPubKey))).toThrow( + "invalid uncompressed x and y coordinates" + ); }); test("pointDecode -> uncompressed valid", async function () { - // Valid uncompressed public key with 00 as the first bit (test against 'x' field of JWK getting truncated) - const uncompPubKey = "0400d2eb47be2006c29db5fe9941dd686d19ddeea85a0328894f08091f6b5be9b366c4872345594c12a7f7c47c62dd8074542934820fce5ee0ddc55d6d1d8dd312" - const jwk = pointDecode(uint8ArrayFromHexString(uncompPubKey)); - expect(jwk.x).toBeDefined(); - expect(jwk.y).toBeDefined(); + // Valid uncompressed public key with 00 as the first bit (test against 'x' field of JWK getting truncated) + const uncompPubKey = + "0400d2eb47be2006c29db5fe9941dd686d19ddeea85a0328894f08091f6b5be9b366c4872345594c12a7f7c47c62dd8074542934820fce5ee0ddc55d6d1d8dd312"; + const jwk = pointDecode(uint8ArrayFromHexString(uncompPubKey)); + expect(jwk.x).toBeDefined(); + expect(jwk.y).toBeDefined(); - // Convert x value to make sure it matches first half of uncompressed key WITHOUT truncating the first 0 bit - let xString: string = jwk.x !== undefined ? jwk.x: "_"; - let xBytes = new Uint8Array(base64urlToBuffer(xString)); - let xHex = uint8ArrayToHexString(xBytes); - expect(xHex).toBe(uncompPubKey.substring(2, 66)) + // Convert x value to make sure it matches first half of uncompressed key WITHOUT truncating the first 0 bit + let xString: string = jwk.x !== undefined ? jwk.x : "_"; + let xBytes = new Uint8Array(base64urlToBuffer(xString)); + let xHex = uint8ArrayToHexString(xBytes); + expect(xHex).toBe(uncompPubKey.substring(2, 66)); - // Convert y value to make sure it's the same as second half of uncompressed key - let yString: string = jwk.y !== undefined ? jwk.y: "_"; - let yBytes = new Uint8Array(base64urlToBuffer(yString)); - let yHex = uint8ArrayToHexString(yBytes); - expect(yHex).toBe(uncompPubKey.substring(66, 130)) + // Convert y value to make sure it's the same as second half of uncompressed key + let yString: string = jwk.y !== undefined ? jwk.y : "_"; + let yBytes = new Uint8Array(base64urlToBuffer(yString)); + let yHex = uint8ArrayToHexString(yBytes); + expect(yHex).toBe(uncompPubKey.substring(66, 130)); }); // Convert base64 url encoded string to an array -- used here to test that output pads correctly and doesn't get truncated -function base64urlToBuffer( - baseurl64String: string - ): ArrayBuffer { - // Base64url to Base64 - const padding = "==".slice(0, (4 - (baseurl64String.length % 4)) % 4); - const base64String = - baseurl64String.replace(/-/g, "+").replace(/_/g, "/") + padding; - - // Base64 to binary string - const str = atob(base64String); - - // Binary string to buffer - const buffer = new ArrayBuffer(str.length); - const byteView = new Uint8Array(buffer); - for (let i = 0; i < str.length; i++) { - byteView[i] = str.charCodeAt(i); - } - return buffer; -} \ No newline at end of file +function base64urlToBuffer(baseurl64String: string): ArrayBuffer { + // Base64url to Base64 + const padding = "==".slice(0, (4 - (baseurl64String.length % 4)) % 4); + const base64String = + baseurl64String.replace(/-/g, "+").replace(/_/g, "/") + padding; + + // Base64 to binary string + const str = atob(base64String); + + // Binary string to buffer + const buffer = new ArrayBuffer(str.length); + const byteView = new Uint8Array(buffer); + for (let i = 0; i < str.length; i++) { + byteView[i] = str.charCodeAt(i); + } + return buffer; +} diff --git a/packages/api-key-stamper/src/tink/elliptic_curves.ts b/packages/api-key-stamper/src/tink/elliptic_curves.ts index a02cbcc87..243cc2765 100644 --- a/packages/api-key-stamper/src/tink/elliptic_curves.ts +++ b/packages/api-key-stamper/src/tink/elliptic_curves.ts @@ -152,7 +152,7 @@ function validateUncompressedXY(x: bigint, y: bigint): boolean { const a = p - BigInt(3); const b = getB(); const rhs = ((x * x + a) * x + b) % p; - const lhs = (y ** BigInt(2)) % p + const lhs = y ** BigInt(2) % p; return lhs === rhs; } @@ -163,12 +163,16 @@ function validateUncompressedXY(x: bigint, y: bigint): boolean { * P-256 only */ export function pointDecode(point: Uint8Array): JsonWebKey { - const fieldSize = fieldSizeInBytes(); const compressedLength = fieldSize + 1; - const uncompressedLength = (2 * fieldSize) + 1; - if (point.length !== compressedLength && point.length !== uncompressedLength) { - throw new Error("Invalid length: point is not in compressed or uncompressed format"); + const uncompressedLength = 2 * fieldSize + 1; + if ( + point.length !== compressedLength && + point.length !== uncompressedLength + ) { + throw new Error( + "Invalid length: point is not in compressed or uncompressed format" + ); } // Decodes point if its length and first bit match the compressed format if ((point[0] === 2 || point[0] === 3) && point.length == compressedLength) { @@ -187,12 +191,20 @@ export function pointDecode(point: Uint8Array): JsonWebKey { ext: true, }; return result; - // Decodes point if its length and first bit match the uncompressed format + // Decodes point if its length and first bit match the uncompressed format } else if (point[0] === 4 && point.length == uncompressedLength) { - const x = byteArrayToInteger(point.subarray(1, fieldSize + 1)) - const y = byteArrayToInteger(point.subarray(fieldSize + 1, (2 * fieldSize) + 1)) + const x = byteArrayToInteger(point.subarray(1, fieldSize + 1)); + const y = byteArrayToInteger( + point.subarray(fieldSize + 1, 2 * fieldSize + 1) + ); const p = getModulus(); - if (x < BigInt(0) || x >= p || y < BigInt(0) || y >= p || !validateUncompressedXY(x, y)) { + if ( + x < BigInt(0) || + x >= p || + y < BigInt(0) || + y >= p || + !validateUncompressedXY(x, y) + ) { throw new Error("invalid uncompressed x and y coordinates"); } const result: JsonWebKey = { @@ -202,7 +214,7 @@ export function pointDecode(point: Uint8Array): JsonWebKey { y: Bytes.toBase64(integerToByteArray(y, 32), /* websafe */ true), ext: true, }; - return result + return result; } throw new Error("invalid format"); } diff --git a/packages/encoding/src/__tests__/index-test.ts b/packages/encoding/src/__tests__/index-test.ts index a190ff19b..f79a6598d 100644 --- a/packages/encoding/src/__tests__/index-test.ts +++ b/packages/encoding/src/__tests__/index-test.ts @@ -114,19 +114,19 @@ test("hexStringToBase64url", async function () { // Test array padding for uint8ArrayFromHexString // Convert hex string to uint8 array and pads it with zeroes to a particular length if its less test("uint8ArrayFromHexString Test padding", async function () { - - // TOO SHORT - test a hex string with less bytes than the "length" parameter provided + // TOO SHORT - test a hex string with less bytes than the "length" parameter provided const hexString = "5234d08dfa2c815f3097b8ba848a28172e85bec78886e8e201afccb166fc"; // length is 30 bytes, so must be padded with 2 0's at the beginning const expectedUint8Array = new Uint8Array([ - 0, 0, 82, 52, 208, 141, 250, 44, 129, 95, 48, 151, 184, 186, 132, 138, 40, 23, 46, - 133, 190, 199, 136, 134, 232, 226, 1, 175, 204, 177, 102, 252, + 0, 0, 82, 52, 208, 141, 250, 44, 129, 95, 48, 151, 184, 186, 132, 138, 40, + 23, 46, 133, 190, 199, 136, 134, 232, 226, 1, 175, 204, 177, 102, 252, ]); expect(uint8ArrayFromHexString(hexString, 32)).toEqual(expectedUint8Array); // Hex string => Uint8Array - // TOO LONG - test a hex string with less bytes than the "length" parameter provided -- Should error const hexString2 = "5234d08dfa2c815f3097b8ba848a28172e85bec78886e8e201afccb166fcfafbfcfd"; // length is 34 bytes, so no additional padding will be added - expect(() => uint8ArrayFromHexString(hexString2, 32)).toThrow("hex value cannot fit in a buffer of 32 byte(s)") -}); \ No newline at end of file + expect(() => uint8ArrayFromHexString(hexString2, 32)).toThrow( + "hex value cannot fit in a buffer of 32 byte(s)" + ); +}); From 5a81f2636ee04eca96af5d9207ec9694e8c7fcdf Mon Sep 17 00:00:00 2001 From: Omkar Shanbhag Date: Thu, 15 Aug 2024 13:38:41 -0400 Subject: [PATCH 12/19] remove comments --- packages/api-key-stamper/src/tink/elliptic_curves.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/api-key-stamper/src/tink/elliptic_curves.ts b/packages/api-key-stamper/src/tink/elliptic_curves.ts index 243cc2765..51db5aba1 100644 --- a/packages/api-key-stamper/src/tink/elliptic_curves.ts +++ b/packages/api-key-stamper/src/tink/elliptic_curves.ts @@ -136,7 +136,6 @@ function getY(x: bigint, lsb: boolean): bigint { } /** -<<<<<<< HEAD * * Given x and y coordinates of a JWK, checks whether these are valid points on * the P-256 elliptic curve. From be40867edc6d2274f306832e384ab6a11b75848b Mon Sep 17 00:00:00 2001 From: Omkar Shanbhag Date: Thu, 15 Aug 2024 14:04:49 -0400 Subject: [PATCH 13/19] prettier --- packages/api-key-stamper/src/tink/elliptic_curves.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/api-key-stamper/src/tink/elliptic_curves.ts b/packages/api-key-stamper/src/tink/elliptic_curves.ts index 51db5aba1..378476c30 100644 --- a/packages/api-key-stamper/src/tink/elliptic_curves.ts +++ b/packages/api-key-stamper/src/tink/elliptic_curves.ts @@ -137,9 +137,9 @@ function getY(x: bigint, lsb: boolean): bigint { /** * - * Given x and y coordinates of a JWK, checks whether these are valid points on - * the P-256 elliptic curve. - * + * Given x and y coordinates of a JWK, checks whether these are valid points on + * the P-256 elliptic curve. + * * P-256 only * * @param x x-coordinate From e75b2f588f1715edd13ec4d37c4c6a021471bb4e Mon Sep 17 00:00:00 2001 From: Omkar Shanbhag Date: Thu, 15 Aug 2024 15:21:04 -0400 Subject: [PATCH 14/19] pr comments --- .../src/tink/elliptic_curves.ts | 2 +- packages/encoding/src/__tests__/index-test.ts | 36 +++++++++---------- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/packages/api-key-stamper/src/tink/elliptic_curves.ts b/packages/api-key-stamper/src/tink/elliptic_curves.ts index 378476c30..8dba2b4ea 100644 --- a/packages/api-key-stamper/src/tink/elliptic_curves.ts +++ b/packages/api-key-stamper/src/tink/elliptic_curves.ts @@ -144,7 +144,7 @@ function getY(x: bigint, lsb: boolean): bigint { * * @param x x-coordinate * @param y y-coordinate - * @return boolean validitiy + * @return boolean validity */ function validateUncompressedXY(x: bigint, y: bigint): boolean { const p = getModulus(); diff --git a/packages/encoding/src/__tests__/index-test.ts b/packages/encoding/src/__tests__/index-test.ts index f79a6598d..559b0bb2c 100644 --- a/packages/encoding/src/__tests__/index-test.ts +++ b/packages/encoding/src/__tests__/index-test.ts @@ -94,6 +94,22 @@ test("uint8ArrayFromHexString", async function () { expect(() => { uint8ArrayFromHexString("0100", 1).toString(); // the number 256 cannot fit into 1 byte }).toThrow("hex value cannot fit in a buffer of 1 byte(s)"); + + // TOO SHORT - test a hex string with less bytes than the "length" parameter provided + const hexString2 = + "5234d08dfa2c815f3097b8ba848a28172e85bec78886e8e201afccb166fc"; // length is 30 bytes, so must be padded with 2 0's at the beginning + const expectedUint8Array2 = new Uint8Array([ + 0, 0, 82, 52, 208, 141, 250, 44, 129, 95, 48, 151, 184, 186, 132, 138, 40, + 23, 46, 133, 190, 199, 136, 134, 232, 226, 1, 175, 204, 177, 102, 252, + ]); + expect(uint8ArrayFromHexString(hexString2, 32)).toEqual(expectedUint8Array2); // Hex string => Uint8Array + + // TOO LONG - test a hex string with less bytes than the "length" parameter provided -- Should error + const hexString3 = + "5234d08dfa2c815f3097b8ba848a28172e85bec78886e8e201afccb166fcfafbfcfd"; // length is 34 bytes, so no additional padding will be added + expect(() => uint8ArrayFromHexString(hexString3, 32)).toThrow( + "hex value cannot fit in a buffer of 32 byte(s)" + ); }); // Test for hexStringToBase64url @@ -110,23 +126,3 @@ test("hexStringToBase64url", async function () { hexStringToBase64url("0100", 1); }).toThrow("hex value cannot fit in a buffer of 1 byte(s)"); }); - -// Test array padding for uint8ArrayFromHexString -// Convert hex string to uint8 array and pads it with zeroes to a particular length if its less -test("uint8ArrayFromHexString Test padding", async function () { - // TOO SHORT - test a hex string with less bytes than the "length" parameter provided - const hexString = - "5234d08dfa2c815f3097b8ba848a28172e85bec78886e8e201afccb166fc"; // length is 30 bytes, so must be padded with 2 0's at the beginning - const expectedUint8Array = new Uint8Array([ - 0, 0, 82, 52, 208, 141, 250, 44, 129, 95, 48, 151, 184, 186, 132, 138, 40, - 23, 46, 133, 190, 199, 136, 134, 232, 226, 1, 175, 204, 177, 102, 252, - ]); - expect(uint8ArrayFromHexString(hexString, 32)).toEqual(expectedUint8Array); // Hex string => Uint8Array - - // TOO LONG - test a hex string with less bytes than the "length" parameter provided -- Should error - const hexString2 = - "5234d08dfa2c815f3097b8ba848a28172e85bec78886e8e201afccb166fcfafbfcfd"; // length is 34 bytes, so no additional padding will be added - expect(() => uint8ArrayFromHexString(hexString2, 32)).toThrow( - "hex value cannot fit in a buffer of 32 byte(s)" - ); -}); From d486dc98b77f5d2f46eb6a113620ce44f0e784d2 Mon Sep 17 00:00:00 2001 From: Omkar Shanbhag Date: Thu, 15 Aug 2024 16:40:25 -0400 Subject: [PATCH 15/19] add tests for createEmbeddedAPIKey --- packages/api-key-stamper/src/index.ts | 4 ++++ packages/sdk-browser/src/__tests__/utils-test.ts | 14 ++++++++++++++ packages/sdk-browser/src/utils.ts | 2 +- 3 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 packages/sdk-browser/src/__tests__/utils-test.ts diff --git a/packages/api-key-stamper/src/index.ts b/packages/api-key-stamper/src/index.ts index f8850b6d9..a6d733672 100644 --- a/packages/api-key-stamper/src/index.ts +++ b/packages/api-key-stamper/src/index.ts @@ -78,3 +78,7 @@ export class ApiKeyStamper { }; } } + +import { pointDecode } from "./tink/elliptic_curves"; + +export { pointDecode }; diff --git a/packages/sdk-browser/src/__tests__/utils-test.ts b/packages/sdk-browser/src/__tests__/utils-test.ts new file mode 100644 index 000000000..4c8dc38ef --- /dev/null +++ b/packages/sdk-browser/src/__tests__/utils-test.ts @@ -0,0 +1,14 @@ +import { test, expect } from "@jest/globals"; +import { createEmbeddedAPIKey } from "../utils"; + +test("createEmbeddedAPIKey", async function () { + // Test valid uncompressed public key + const uncompPubKey = + "0400d2eb47be2006c29db5fe9941dd686d19ddeea85a0328894f08091f6b5be9b366c4872345594c12a7f7c47c62dd8074542934820fce5ee0ddc55d6d1d8dd312"; + await expect(createEmbeddedAPIKey(uncompPubKey)).resolves.not.toThrow(); + + // test invalid uncompressed public key (last byte has been changed) + const uncompPubKeyInvalid = + "0400d2eb47be2006c29db5fe9941dd686d19ddeea85a0328894f08091f6b5be9b366c4872345594c12a7f7c47c62dd8074542934820fce5ee0ddc55d6d1d8dd311"; + await expect(createEmbeddedAPIKey(uncompPubKeyInvalid)).rejects.toThrow(); +}); diff --git a/packages/sdk-browser/src/utils.ts b/packages/sdk-browser/src/utils.ts index 0f282dc5d..e99b3983a 100644 --- a/packages/sdk-browser/src/utils.ts +++ b/packages/sdk-browser/src/utils.ts @@ -10,7 +10,7 @@ import bs58check from "bs58check"; import { AeadId, CipherSuite, KdfId, KemId } from "hpke-js"; import type { EmbeddedAPIKey } from "./models"; -import { pointDecode } from "@turnkey/api-key-stamper/src/tink/elliptic_curves"; +import { pointDecode } from "@turnkey/api-key-stamper"; // createEmbeddedAPIKey creates an embedded API key encrypted to a target key (typically embedded within an iframe). // This returns a bundle that can be decrypted by that target key, as well as the public key of the newly created API key. From b30fa8d2b1c46266eb65dd76821471908edc41a1 Mon Sep 17 00:00:00 2001 From: Omkar Shanbhag Date: Thu, 15 Aug 2024 17:31:18 -0400 Subject: [PATCH 16/19] add tests for compressed key --- .../src/__tests__/elliptic-curves-test.ts | 25 +++++++++++++++++++ .../src/tink/elliptic_curves.ts | 6 +++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/packages/api-key-stamper/src/__tests__/elliptic-curves-test.ts b/packages/api-key-stamper/src/__tests__/elliptic-curves-test.ts index b21d6e73e..6b7cb2b4b 100644 --- a/packages/api-key-stamper/src/__tests__/elliptic-curves-test.ts +++ b/packages/api-key-stamper/src/__tests__/elliptic-curves-test.ts @@ -35,6 +35,31 @@ test("pointDecode -> uncompressed valid", async function () { expect(yHex).toBe(uncompPubKey.substring(66, 130)); }); +test("pointDecode -> compressed", async function () { + // Valid compressed public key with 00 as the first bit (test against 'x' field of JWK getting truncated) + const uncompPubKey = + "0400d2eb47be2006c29db5fe9941dd686d19ddeea85a0328894f08091f6b5be9b366c4872345594c12a7f7c47c62dd8074542934820fce5ee0ddc55d6d1d8dd312"; + // corresponding compressed public key + const compPubKey = + "0200d2eb47be2006c29db5fe9941dd686d19ddeea85a0328894f08091f6b5be9b3"; + + const jwk = pointDecode(uint8ArrayFromHexString(compPubKey)); + expect(jwk.x).toBeDefined(); + expect(jwk.y).toBeDefined(); + + // Convert x value to make sure it matches first half of uncompressed key WITHOUT truncating the first 0 bit + let xString: string = jwk.x !== undefined ? jwk.x : "_"; + let xBytes = new Uint8Array(base64urlToBuffer(xString)); + let xHex = uint8ArrayToHexString(xBytes); + expect(xHex).toBe(uncompPubKey.substring(2, 66)); + + // Convert y value to make sure it's the same as second half of uncompressed key + let yString: string = jwk.y !== undefined ? jwk.y : "_"; + let yBytes = new Uint8Array(base64urlToBuffer(yString)); + let yHex = uint8ArrayToHexString(yBytes); + expect(yHex).toBe(uncompPubKey.substring(66, 130)); +}); + // Convert base64 url encoded string to an array -- used here to test that output pads correctly and doesn't get truncated function base64urlToBuffer(baseurl64String: string): ArrayBuffer { // Base64url to Base64 diff --git a/packages/api-key-stamper/src/tink/elliptic_curves.ts b/packages/api-key-stamper/src/tink/elliptic_curves.ts index 8dba2b4ea..9ce77632d 100644 --- a/packages/api-key-stamper/src/tink/elliptic_curves.ts +++ b/packages/api-key-stamper/src/tink/elliptic_curves.ts @@ -1,6 +1,8 @@ /** * Code modified from https://github.com/google/tink/blob/6f74b99a2bfe6677e3670799116a57268fd067fa/javascript/subtle/elliptic_curves.ts * - The implementation of integerToByteArray has been modified to augment the resulting byte array to a certain length. + * - The implementation of PointDecode has been modified to decode both compressed and uncompressed points by checking for correct format + * - Methoed isP256CurvePoint added to check whether an uncompressed point is valid * * @license * Copyright 2020 Google LLC @@ -146,7 +148,7 @@ function getY(x: bigint, lsb: boolean): bigint { * @param y y-coordinate * @return boolean validity */ -function validateUncompressedXY(x: bigint, y: bigint): boolean { +function isP256CurvePoint(x: bigint, y: bigint): boolean { const p = getModulus(); const a = p - BigInt(3); const b = getB(); @@ -202,7 +204,7 @@ export function pointDecode(point: Uint8Array): JsonWebKey { x >= p || y < BigInt(0) || y >= p || - !validateUncompressedXY(x, y) + !isP256CurvePoint(x, y) ) { throw new Error("invalid uncompressed x and y coordinates"); } From e491d2c44b62c9fb9b4c9aa9dc2b622637c41991 Mon Sep 17 00:00:00 2001 From: Omkar Shanbhag Date: Fri, 16 Aug 2024 11:00:08 -0400 Subject: [PATCH 17/19] pr review --- packages/api-key-stamper/src/index.ts | 4 +--- packages/api-key-stamper/src/tink/elliptic_curves.ts | 2 +- packages/sdk-browser/src/__tests__/utils-test.ts | 10 ++++++++++ 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/api-key-stamper/src/index.ts b/packages/api-key-stamper/src/index.ts index a6d733672..c62369326 100644 --- a/packages/api-key-stamper/src/index.ts +++ b/packages/api-key-stamper/src/index.ts @@ -79,6 +79,4 @@ export class ApiKeyStamper { } } -import { pointDecode } from "./tink/elliptic_curves"; - -export { pointDecode }; +export { pointDecode } from "./tink/elliptic_curves"; diff --git a/packages/api-key-stamper/src/tink/elliptic_curves.ts b/packages/api-key-stamper/src/tink/elliptic_curves.ts index 9ce77632d..4fd970b72 100644 --- a/packages/api-key-stamper/src/tink/elliptic_curves.ts +++ b/packages/api-key-stamper/src/tink/elliptic_curves.ts @@ -2,7 +2,7 @@ * Code modified from https://github.com/google/tink/blob/6f74b99a2bfe6677e3670799116a57268fd067fa/javascript/subtle/elliptic_curves.ts * - The implementation of integerToByteArray has been modified to augment the resulting byte array to a certain length. * - The implementation of PointDecode has been modified to decode both compressed and uncompressed points by checking for correct format - * - Methoed isP256CurvePoint added to check whether an uncompressed point is valid + * - Method isP256CurvePoint added to check whether an uncompressed point is valid * * @license * Copyright 2020 Google LLC diff --git a/packages/sdk-browser/src/__tests__/utils-test.ts b/packages/sdk-browser/src/__tests__/utils-test.ts index 4c8dc38ef..1190281a4 100644 --- a/packages/sdk-browser/src/__tests__/utils-test.ts +++ b/packages/sdk-browser/src/__tests__/utils-test.ts @@ -1,5 +1,6 @@ import { test, expect } from "@jest/globals"; import { createEmbeddedAPIKey } from "../utils"; +import { create } from "domain"; test("createEmbeddedAPIKey", async function () { // Test valid uncompressed public key @@ -7,6 +8,15 @@ test("createEmbeddedAPIKey", async function () { "0400d2eb47be2006c29db5fe9941dd686d19ddeea85a0328894f08091f6b5be9b366c4872345594c12a7f7c47c62dd8074542934820fce5ee0ddc55d6d1d8dd312"; await expect(createEmbeddedAPIKey(uncompPubKey)).resolves.not.toThrow(); + const embAPIKey = await createEmbeddedAPIKey(uncompPubKey); + expect(embAPIKey.authBundle).toBeDefined(); + expect(embAPIKey.authBundle).not.toBeNull(); + expect(embAPIKey.authBundle).not.toEqual(""); + + expect(embAPIKey.publicKey).toBeDefined(); + expect(embAPIKey.publicKey).not.toBeNull(); + expect(embAPIKey.authBundle).not.toEqual(""); + // test invalid uncompressed public key (last byte has been changed) const uncompPubKeyInvalid = "0400d2eb47be2006c29db5fe9941dd686d19ddeea85a0328894f08091f6b5be9b366c4872345594c12a7f7c47c62dd8074542934820fce5ee0ddc55d6d1d8dd311"; From 30c94b87d6c2272bc5ad59884ca88bf013cf11c8 Mon Sep 17 00:00:00 2001 From: Omkar Shanbhag Date: Fri, 16 Aug 2024 11:04:19 -0400 Subject: [PATCH 18/19] remove erroneous auto import --- packages/sdk-browser/src/__tests__/utils-test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/sdk-browser/src/__tests__/utils-test.ts b/packages/sdk-browser/src/__tests__/utils-test.ts index 1190281a4..dc7fc036d 100644 --- a/packages/sdk-browser/src/__tests__/utils-test.ts +++ b/packages/sdk-browser/src/__tests__/utils-test.ts @@ -1,6 +1,5 @@ import { test, expect } from "@jest/globals"; import { createEmbeddedAPIKey } from "../utils"; -import { create } from "domain"; test("createEmbeddedAPIKey", async function () { // Test valid uncompressed public key From f4b607f36fba5ea17b240f3988217bd23f51e1d8 Mon Sep 17 00:00:00 2001 From: Omkar Shanbhag Date: Fri, 16 Aug 2024 13:15:20 -0400 Subject: [PATCH 19/19] changeset added --- .changeset/quiet-beans-chew.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .changeset/quiet-beans-chew.md diff --git a/.changeset/quiet-beans-chew.md b/.changeset/quiet-beans-chew.md new file mode 100644 index 000000000..55a82bc43 --- /dev/null +++ b/.changeset/quiet-beans-chew.md @@ -0,0 +1,11 @@ +--- +"@turnkey/api-key-stamper": patch +"@turnkey/sdk-browser": patch +"@turnkey/encoding": patch +--- + +Updates to various libraries to protect against JWK truncation: + +- `@turnkey/api-key-stamper`: Add functionality for verifying and padding uncompressed public keys while generating JWK's +- `@turnkey/sdk-browser`: Use code for verifying and padding uncompressed public keys while creating passkey sessions +- `@turnkey/encoding`: Updating tests for some libraries