diff --git a/.changeset/sour-trains-change.md b/.changeset/sour-trains-change.md new file mode 100644 index 00000000..0c71e459 --- /dev/null +++ b/.changeset/sour-trains-change.md @@ -0,0 +1,5 @@ +--- +"@nx.js/runtime": patch +--- + +Add support for "SHA-384" and "SHA-512" in `crypto.subtle.digest()` diff --git a/.changeset/strong-cobras-cross.md b/.changeset/strong-cobras-cross.md new file mode 100644 index 00000000..b1fd59b0 --- /dev/null +++ b/.changeset/strong-cobras-cross.md @@ -0,0 +1,5 @@ +--- +"@nx.js/runtime": patch +--- + +Implement `importKey()`, `encrypt()`, and `decrypt()` functions for `crypto.subtle` (AES-CBC and AES-XTS modes) diff --git a/apps/tests/src/crypto.test.ts b/apps/tests/src/crypto.test.ts index a758d4cf..1613332d 100644 --- a/apps/tests/src/crypto.test.ts +++ b/apps/tests/src/crypto.test.ts @@ -3,13 +3,31 @@ import * as assert from 'uvu/assert'; const test = suite('crypto'); -function toHex(a: ArrayBuffer) { - const u = new Uint8Array(a); - const s = []; - for (let i = 0; i < u.length; i++) { - s.push(u[i].toString(16).padStart(2, '0')); +const isNXJS = typeof Switch !== 'undefined'; + +function toHex(arr: ArrayBuffer) { + return Array.from(new Uint8Array(arr)) + .map((v) => v.toString(16).padStart(2, '0')) + .join(''); +} + +function fromHex(hex: string): ArrayBuffer { + const arr = new Uint8Array(hex.length / 2); + for (let i = 0; i < hex.length; i += 2) { + arr[i / 2] = parseInt(hex.substr(i, 2), 16); } - return s.join(''); + return arr.buffer; +} + +function concat(...buffers: ArrayBuffer[]): ArrayBuffer { + const size = buffers.reduce((acc, buf) => acc + buf.byteLength, 0); + let offset = 0; + const result = new Uint8Array(size); + for (const buf of buffers) { + result.set(new Uint8Array(buf), offset); + offset += buf.byteLength; + } + return result.buffer; } test('`crypto.getRandomValues()`', () => { @@ -34,4 +52,248 @@ test("`crypto.subtle.digest('sha-256')`", async () => { ); }); +test("`crypto.subtle.digest('sha-384')`", async () => { + const data = new TextEncoder().encode('hello'); + const digest = await crypto.subtle.digest('sha-384', data); + assert.equal( + toHex(digest), + '59e1748777448c69de6b800d7a33bbfb9ff1b463e44354c3553bcdb9c666fa90125a3c79f90397bdf5f6a13de828684f', + ); +}); + +test("`crypto.subtle.digest('sha-512')`", async () => { + const data = new TextEncoder().encode('hello'); + const digest = await crypto.subtle.digest('sha-512', data); + assert.equal( + toHex(digest), + '9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043', + ); +}); + +test("`crypto.subtle.importKey()` with 'raw' format and 'AES-CBC' algorithm, 128-bit key", async () => { + const keyData = new Uint8Array([ + 188, 136, 184, 200, 227, 200, 149, 203, 33, 186, 60, 145, 54, 19, 92, 88, + ]); + + const key = await crypto.subtle.importKey( + 'raw', + keyData, + { name: 'AES-CBC' }, + false, + ['encrypt', 'decrypt'], + ); + + assert.instance(key, CryptoKey); + assert.equal(key.extractable, false); + assert.equal(key.algorithm.name, 'AES-CBC'); + // @ts-expect-error `length` is not defined on `KeyAlgorithm` + assert.equal(key.algorithm.length, 128); + + // Sorted because bun / deno disagree on the order + const usages = key.usages.slice().sort(); + assert.equal(usages.length, 2); + assert.equal(usages[0], 'decrypt'); + assert.equal(usages[1], 'encrypt'); +}); + +test("`crypto.subtle.importKey()` with 'raw' format and 'AES-CBC' algorithm, 256-bit key", async () => { + const keyData = new Uint8Array([ + 194, 180, 97, 245, 222, 0, 208, 29, 177, 74, 94, 95, 124, 172, 123, 89, 168, + 89, 145, 158, 128, 61, 7, 182, 192, 90, 250, 33, 24, 44, 24, 108, + ]); + + const key = await crypto.subtle.importKey('raw', keyData, 'AES-CBC', false, [ + 'decrypt', + ]); + + assert.instance(key, CryptoKey); + assert.equal(key.usages.length, 1); + assert.equal(key.usages[0], 'decrypt'); + assert.equal(key.algorithm.name, 'AES-CBC'); + // @ts-expect-error `length` is not defined on `KeyAlgorithm` + assert.equal(key.algorithm.length, 256); +}); + +test("`crypto.subtle.encrypt()` with 'AES-CBC' algorithm, 128-bit key", async () => { + const keyData = new Uint8Array([ + 188, 136, 184, 200, 227, 200, 149, 203, 33, 186, 60, 145, 54, 19, 92, 88, + ]); + const iv = new Uint8Array([ + 38, 89, 172, 231, 98, 165, 172, 212, 137, 184, 41, 162, 105, 26, 119, 158, + ]); + + const key = await crypto.subtle.importKey( + 'raw', + keyData, + { name: 'AES-CBC' }, + false, + ['encrypt'], + ); + + const ciphertext = await crypto.subtle.encrypt( + { name: 'AES-CBC', iv: iv.buffer }, + key, + new TextEncoder().encode('hello'), + ); + + assert.instance(ciphertext, ArrayBuffer); + assert.equal(toHex(ciphertext), '4b4fddd4b88f2e6a36500f89aa177d0d'); +}); + +test("`crypto.subtle.encrypt()` with 'AES-CBC' algorithm, 256-bit key", async () => { + const keyData = new Uint8Array([ + 194, 180, 97, 245, 222, 0, 208, 29, 177, 74, 94, 95, 124, 172, 123, 89, 168, + 89, 145, 158, 128, 61, 7, 182, 192, 90, 250, 33, 24, 44, 24, 108, + ]); + const iv = new Uint8Array([ + 199, 76, 237, 213, 127, 137, 216, 106, 243, 237, 191, 146, 158, 226, 56, + 143, + ]); + + const key = await crypto.subtle.importKey( + 'raw', + keyData, + { name: 'AES-CBC' }, + false, + ['encrypt'], + ); + + const ciphertext = await crypto.subtle.encrypt( + { name: 'AES-CBC', iv }, + key, + new TextEncoder().encode('hello').buffer, + ); + + assert.instance(ciphertext, ArrayBuffer); + assert.equal(toHex(ciphertext), 'da7299d52ef669a5e513b801fb65ef06'); +}); + +test("`crypto.subtle.decrypt()` with 'AES-CBC' algorithm, 128-bit key", async () => { + const keyData = new Uint8Array([ + 188, 136, 184, 200, 227, 200, 149, 203, 33, 186, 60, 145, 54, 19, 92, 88, + ]); + const iv = new Uint8Array([ + 38, 89, 172, 231, 98, 165, 172, 212, 137, 184, 41, 162, 105, 26, 119, 158, + ]); + + const key = await crypto.subtle.importKey('raw', keyData, 'AES-CBC', false, [ + 'decrypt', + ]); + + const plaintext = await crypto.subtle.decrypt( + { name: 'AES-CBC', iv }, + key, + fromHex('4b4fddd4b88f2e6a36500f89aa177d0d'), + ); + + assert.instance(plaintext, ArrayBuffer); + assert.equal(new TextDecoder().decode(new Uint8Array(plaintext)), 'hello'); +}); + +test("`crypto.subtle.decrypt()` with 'AES-CBC' algorithm, 256-bit key", async () => { + const keyData = new Uint8Array([ + 194, 180, 97, 245, 222, 0, 208, 29, 177, 74, 94, 95, 124, 172, 123, 89, 168, + 89, 145, 158, 128, 61, 7, 182, 192, 90, 250, 33, 24, 44, 24, 108, + ]); + const iv = new Uint8Array([ + 199, 76, 237, 213, 127, 137, 216, 106, 243, 237, 191, 146, 158, 226, 56, + 143, + ]); + + const key = await crypto.subtle.importKey( + 'raw', + keyData, + { name: 'AES-CBC' }, + false, + ['decrypt'], + ); + + const plaintext = await crypto.subtle.decrypt( + { name: 'AES-CBC', iv }, + key, + fromHex('da7299d52ef669a5e513b801fb65ef06'), + ); + + assert.instance(plaintext, ArrayBuffer); + assert.equal(new TextDecoder().decode(new Uint8Array(plaintext)), 'hello'); +}); + +// Non-standard APIs that are only going to work on nx.js +if (isNXJS) { + test("`crypto.subtle.importKey()` with 'raw' format and 'AES-XTS' algorithm, two 128-bit keys", async () => { + const key0 = fromHex('0a316b344a7bc7b4db239c61017b86f7'); + const key1 = fromHex('cc4f4df7cb2becb9a307bb17e8eb01f3'); + + const key = await crypto.subtle.importKey( + 'raw', + concat(key0, key1), + { name: 'AES-XTS' }, + true, + ['encrypt'], + ); + + assert.instance(key, CryptoKey); + assert.equal(key.extractable, true); + assert.equal(key.usages.length, 1); + assert.equal(key.usages[0], 'encrypt'); + assert.equal(key.algorithm.name, 'AES-XTS'); + // @ts-expect-error `length` is not defined on `KeyAlgorithm` + assert.equal(key.algorithm.length, 256); + }); + + test("`crypto.subtle.encrypt()` with 'AES-XTS' algorithm, two 128-bit keys (aligned with AES block size)", async () => { + const key0 = fromHex('0a316b344a7bc7b4db239c61017b86f7'); + const key1 = fromHex('cc4f4df7cb2becb9a307bb17e8eb01f3'); + + const key = await crypto.subtle.importKey( + 'raw', + new BigUint64Array(concat(key0, key1)), + 'AES-XTS', + true, + ['encrypt'], + ); + + const text = 'This is some text that aligns with 48 bytes!!!!!'; + const plaintext = new TextEncoder().encode(text); + assert.equal(plaintext.byteLength, 48); + + const ciphertext = await crypto.subtle.encrypt( + { name: 'AES-XTS', sectorSize: 16 }, + key, + plaintext, + ); + + assert.instance(ciphertext, ArrayBuffer); + assert.equal( + toHex(ciphertext), + 'b55622812044704bf3f0565ec78d3adfa5fee13aaf030467c2d5c084184989267583c148a883a0ea73d9da63fcf3f4bf', + ); + }); + + test("`crypto.subtle.decrypt()` with 'AES-XTS' algorithm, two 128-bit keys (aligned with AES block size)", async () => { + const key0 = fromHex('0a316b344a7bc7b4db239c61017b86f7'); + const key1 = fromHex('cc4f4df7cb2becb9a307bb17e8eb01f3'); + + const key = await crypto.subtle.importKey( + 'raw', + concat(key0, key1), + 'AES-XTS', + true, + ['decrypt'], + ); + + const plaintext = await crypto.subtle.decrypt( + { name: 'AES-XTS', sectorSize: 16 }, + key, + fromHex( + 'b55622812044704bf3f0565ec78d3adfa5fee13aaf030467c2d5c084184989267583c148a883a0ea73d9da63fcf3f4bf', + ), + ); + + assert.instance(plaintext, ArrayBuffer); + const text = 'This is some text that aligns with 48 bytes!!!!!'; + assert.equal(new TextDecoder().decode(new Uint8Array(plaintext)), text); + }); +} + test.run(); diff --git a/packages/runtime/src/$.ts b/packages/runtime/src/$.ts index b4127f47..71e2dfa2 100644 --- a/packages/runtime/src/$.ts +++ b/packages/runtime/src/$.ts @@ -40,6 +40,8 @@ import type { URL, URLSearchParams } from './polyfills/url'; import type { DOMPoint, DOMPointInit } from './dompoint'; import type { DOMMatrix, DOMMatrixReadOnly, DOMMatrixInit } from './dommatrix'; import type { Gamepad, GamepadButton } from './navigator/gamepad'; +import type { CryptoKey, SubtleCrypto } from './crypto'; +import type { Algorithm } from './types'; import type { PromiseState } from '@nx.js/inspect'; type ClassOf = { @@ -119,7 +121,20 @@ export interface Init { ): number[]; // crypto.c - cryptoDigest(algorithm: string, buf: ArrayBuffer): Promise; + cryptoKeyNew( + algorithm: Algorithm, + key: ArrayBuffer, + extractable: boolean, + keyUsages: KeyUsage[], + ): CryptoKey; + cryptoKeyInit(c: ClassOf): void; + cryptoSubtleInit(c: ClassOf): void; + cryptoEncrypt( + algorithm: Algorithm, + key: CryptoKey, + data: BufferSource, + ): Promise; + cryptoDigest(algorithm: string, buf: BufferSource): Promise; cryptoRandomBytes(buf: ArrayBuffer, offset: number, length: number): void; sha256Hex(str: string): string; diff --git a/packages/runtime/src/crypto.ts b/packages/runtime/src/crypto.ts index 8c472856..4b18872a 100644 --- a/packages/runtime/src/crypto.ts +++ b/packages/runtime/src/crypto.ts @@ -1,18 +1,32 @@ import { $ } from './$'; import { INTERNAL_SYMBOL } from './internal'; -import { - assertInternalConstructor, - bufferSourceToArrayBuffer, - createInternal, - def, -} from './utils'; -import type { ArrayBufferView, BufferSource } from './types'; +import { assertInternalConstructor, createInternal, def, stub } from './utils'; +import { CryptoKey } from './crypto/crypto-key'; +import type { + AesCbcParams, + AesCtrParams, + AesDerivedKeyParams, + AesGcmParams, + AesKeyAlgorithm, + AesKeyGenParams, + AesXtsParams, + ArrayBufferView, + BufferSource, + EcKeyGenParams, + EcKeyImportParams, + EcdhKeyDeriveParams, + EcdsaParams, + HmacImportParams, + HmacKeyGenParams, + JsonWebKey, + KeyFormat, + KeyUsage, + RsaHashedImportParams, + RsaHashedKeyGenParams, + RsaOaepParams, +} from './types'; -export interface Algorithm { - name: string; -} - -export type AlgorithmIdentifier = Algorithm | string; +export * from './crypto/crypto-key'; interface CryptoInternal { subtle?: SubtleCrypto; @@ -121,18 +135,22 @@ export class SubtleCrypto implements globalThis.SubtleCrypto { assertInternalConstructor(arguments); } + /** + * Decrypts some encrypted data. + * + * It takes as arguments a key to decrypt with, some optional extra parameters, and the data to decrypt (also known as "ciphertext"). + * + * @returns A Promise which will be fulfilled with the decrypted data (also known as "plaintext") as an `ArrayBuffer`. + * @see https://developer.mozilla.org/docs/Web/API/SubtleCrypto/decrypt + */ decrypt( - algorithm: - | AlgorithmIdentifier - | RsaOaepParams - | AesCtrParams - | AesCbcParams - | AesGcmParams, + algorithm: AesCbcParams | AesXtsParams, key: CryptoKey, data: BufferSource, ): Promise { - throw new Error('Method not implemented.'); + stub(); } + deriveBits( algorithm: | AlgorithmIdentifier @@ -144,6 +162,7 @@ export class SubtleCrypto implements globalThis.SubtleCrypto { ): Promise { throw new Error('Method not implemented.'); } + deriveKey( algorithm: | AlgorithmIdentifier @@ -159,29 +178,6 @@ export class SubtleCrypto implements globalThis.SubtleCrypto { | HmacImportParams, extractable: boolean, keyUsages: KeyUsage[], - ): Promise; - deriveKey( - algorithm: - | AlgorithmIdentifier - | EcdhKeyDeriveParams - | HkdfParams - | Pbkdf2Params, - baseKey: CryptoKey, - derivedKeyType: - | AlgorithmIdentifier - | HkdfParams - | Pbkdf2Params - | AesDerivedKeyParams - | HmacImportParams, - extractable: boolean, - keyUsages: Iterable, - ): Promise; - deriveKey( - algorithm: unknown, - baseKey: unknown, - derivedKeyType: unknown, - extractable: unknown, - keyUsages: unknown, ): Promise { throw new Error('Method not implemented.'); } @@ -214,22 +210,18 @@ export class SubtleCrypto implements globalThis.SubtleCrypto { ): Promise { return $.cryptoDigest( typeof algorithm === 'string' ? algorithm : algorithm.name, - bufferSourceToArrayBuffer(data), + data, ); } - encrypt( - algorithm: - | AlgorithmIdentifier - | RsaOaepParams - | AesCtrParams - | AesCbcParams - | AesGcmParams, + async encrypt( + algorithm: AesCbcParams | AesXtsParams, key: CryptoKey, data: BufferSource, ): Promise { - throw new Error('Method not implemented.'); + return $.cryptoEncrypt(normalizeAlgorithm(algorithm), key, data); } + exportKey(format: 'jwk', key: CryptoKey): Promise; exportKey( format: 'pkcs8' | 'raw' | 'spki', @@ -238,16 +230,10 @@ export class SubtleCrypto implements globalThis.SubtleCrypto { exportKey( format: KeyFormat, key: CryptoKey, - ): Promise; - exportKey( - format: unknown, - key: unknown, - ): - | Promise - | Promise - | Promise { + ): Promise { throw new Error('Method not implemented.'); } + generateKey( algorithm: 'Ed25519', extractable: boolean, @@ -278,21 +264,20 @@ export class SubtleCrypto implements globalThis.SubtleCrypto { extractable: boolean, keyUsages: readonly KeyUsage[], ): Promise; - generateKey( - algorithm: AlgorithmIdentifier, - extractable: boolean, - keyUsages: Iterable, - ): Promise; generateKey( algorithm: unknown, extractable: unknown, keyUsages: unknown, - ): - | Promise - | Promise - | Promise { + ): Promise { throw new Error('Method not implemented.'); } + + /** + * Takes as input a key in an external, portable format and gives you a + * {@link CryptoKey} object that you can use in the Web Crypto API. + * + * @see https://developer.mozilla.org/docs/Web/API/SubtleCrypto/importKey + */ importKey( format: 'jwk', keyData: JsonWebKey, @@ -303,7 +288,7 @@ export class SubtleCrypto implements globalThis.SubtleCrypto { | EcKeyImportParams | AesKeyAlgorithm, extractable: boolean, - keyUsages: readonly KeyUsage[], + keyUsages: KeyUsage[], ): Promise; importKey( format: 'pkcs8' | 'raw' | 'spki', @@ -317,21 +302,9 @@ export class SubtleCrypto implements globalThis.SubtleCrypto { extractable: boolean, keyUsages: KeyUsage[], ): Promise; - importKey( - format: 'jwk', - keyData: JsonWebKey, - algorithm: - | AlgorithmIdentifier - | HmacImportParams - | RsaHashedImportParams - | EcKeyImportParams - | AesKeyAlgorithm, - extractable: boolean, - keyUsages: readonly KeyUsage[], - ): Promise; - importKey( - format: 'pkcs8' | 'raw' | 'spki', - keyData: BufferSource, + async importKey( + format: KeyFormat, + keyData: BufferSource | JsonWebKey, algorithm: | AlgorithmIdentifier | HmacImportParams @@ -339,17 +312,26 @@ export class SubtleCrypto implements globalThis.SubtleCrypto { | EcKeyImportParams | AesKeyAlgorithm, extractable: boolean, - keyUsages: Iterable, - ): Promise; - importKey( - format: unknown, - keyData: unknown, - algorithm: unknown, - extractable: unknown, - keyUsages: unknown, + keyUsages: KeyUsage[], ): Promise { - throw new Error('Method not implemented.'); + if (format !== 'raw') { + // Only "raw" format is supported at this time + throw new TypeError( + `Failed to execute 'importKey' on 'SubtleCrypto': 1st argument value '${format}' is not a valid enum value of type KeyFormat.`, + ); + } + const algo = + typeof algorithm === 'string' ? { name: algorithm } : algorithm; + return new CryptoKey( + // @ts-expect-error Internal constructor + INTERNAL_SYMBOL, + algo, + keyData, + extractable, + keyUsages, + ); } + sign( algorithm: AlgorithmIdentifier | RsaPssParams | EcdsaParams, key: CryptoKey, @@ -357,6 +339,7 @@ export class SubtleCrypto implements globalThis.SubtleCrypto { ): Promise { throw new Error('Method not implemented.'); } + unwrapKey( format: KeyFormat, wrappedKey: BufferSource, @@ -375,37 +358,10 @@ export class SubtleCrypto implements globalThis.SubtleCrypto { | AesKeyAlgorithm, extractable: boolean, keyUsages: KeyUsage[], - ): Promise; - unwrapKey( - format: KeyFormat, - wrappedKey: BufferSource, - unwrappingKey: CryptoKey, - unwrapAlgorithm: - | AlgorithmIdentifier - | RsaOaepParams - | AesCtrParams - | AesCbcParams - | AesGcmParams, - unwrappedKeyAlgorithm: - | AlgorithmIdentifier - | HmacImportParams - | RsaHashedImportParams - | EcKeyImportParams - | AesKeyAlgorithm, - extractable: boolean, - keyUsages: Iterable, - ): Promise; - unwrapKey( - format: unknown, - wrappedKey: unknown, - unwrappingKey: unknown, - unwrapAlgorithm: unknown, - unwrappedKeyAlgorithm: unknown, - extractable: unknown, - keyUsages: unknown, ): Promise { throw new Error('Method not implemented.'); } + verify( algorithm: AlgorithmIdentifier | RsaPssParams | EcdsaParams, key: CryptoKey, @@ -414,6 +370,7 @@ export class SubtleCrypto implements globalThis.SubtleCrypto { ): Promise { throw new Error('Method not implemented.'); } + wrapKey( format: KeyFormat, key: CryptoKey, @@ -428,4 +385,9 @@ export class SubtleCrypto implements globalThis.SubtleCrypto { throw new Error('Method not implemented.'); } } +$.cryptoSubtleInit(SubtleCrypto); def(SubtleCrypto); + +function normalizeAlgorithm(algorithm: AlgorithmIdentifier): Algorithm { + return typeof algorithm === 'string' ? { name: algorithm } : algorithm; +} diff --git a/packages/runtime/src/crypto/crypto-key.ts b/packages/runtime/src/crypto/crypto-key.ts new file mode 100644 index 00000000..9dddb867 --- /dev/null +++ b/packages/runtime/src/crypto/crypto-key.ts @@ -0,0 +1,29 @@ +import { $ } from '../$'; +import { inspect } from '../switch/inspect'; +import { assertInternalConstructor, def, proto } from '../utils'; +import type { KeyAlgorithm, KeyType, KeyUsage } from '../types'; + +export class CryptoKey implements globalThis.CryptoKey { + declare readonly algorithm: KeyAlgorithm; + declare readonly extractable: boolean; + declare readonly type: KeyType; + declare readonly usages: KeyUsage[]; + + /** + * @private + */ + constructor() { + assertInternalConstructor(arguments); + return proto( + $.cryptoKeyNew(arguments[1], arguments[2], arguments[3], arguments[4]), + CryptoKey, + ); + } +} +$.cryptoKeyInit(CryptoKey); +def(CryptoKey); + +Object.defineProperty(CryptoKey.prototype, inspect.keys, { + enumerable: false, + value: () => ['type', 'extractable', 'algorithm', 'usages'], +}); diff --git a/packages/runtime/src/types.ts b/packages/runtime/src/types.ts index cc399bf8..8cb984bf 100644 --- a/packages/runtime/src/types.ts +++ b/packages/runtime/src/types.ts @@ -119,3 +119,131 @@ export interface GamepadEffectParameters { strongMagnitude?: number; weakMagnitude?: number; } + +export interface AesCbcParams { + name: 'AES-CBC'; + iv: BufferSource; +} +export interface AesCtrParams extends Algorithm { + counter: BufferSource; + length: number; +} +export interface AesXtsParams { + name: 'AES-XTS'; + sectorSize: number; + sector?: number; + isNintendo?: boolean; +} +export interface AesDerivedKeyParams extends Algorithm { + length: number; +} +export interface AesGcmParams extends Algorithm { + additionalData?: BufferSource; + iv: BufferSource; + tagLength?: number; +} +export interface AesKeyAlgorithm extends KeyAlgorithm { + length: number; +} +export interface AesKeyGenParams extends Algorithm { + length: number; +} +export interface Algorithm { + name: string; +} +export interface EcKeyAlgorithm extends KeyAlgorithm { + namedCurve: NamedCurve; +} +export interface EcKeyGenParams extends Algorithm { + namedCurve: NamedCurve; +} +export interface EcKeyImportParams extends Algorithm { + namedCurve: NamedCurve; +} +export interface EcdhKeyDeriveParams extends Algorithm { + public: CryptoKey; +} +export interface EcdsaParams extends Algorithm { + hash: HashAlgorithmIdentifier; +} +export interface HkdfParams extends Algorithm { + hash: HashAlgorithmIdentifier; + info: BufferSource; + salt: BufferSource; +} +export interface HmacImportParams extends Algorithm { + hash: HashAlgorithmIdentifier; + length?: number; +} +export interface HmacKeyAlgorithm extends KeyAlgorithm { + hash: KeyAlgorithm; + length: number; +} +export interface HmacKeyGenParams extends Algorithm { + hash: HashAlgorithmIdentifier; + length?: number; +} +export interface JsonWebKey { + alg?: string; + crv?: string; + d?: string; + dp?: string; + dq?: string; + e?: string; + ext?: boolean; + k?: string; + key_ops?: string[]; + kty?: string; + n?: string; + oth?: RsaOtherPrimesInfo[]; + p?: string; + q?: string; + qi?: string; + use?: string; + x?: string; + y?: string; +} +export interface KeyAlgorithm { + name: string; +} +export type NamedCurve = string; +export type AlgorithmIdentifier = Algorithm | string; +export type KeyFormat = 'jwk' | 'pkcs8' | 'raw' | 'spki'; +export type KeyType = 'private' | 'public' | 'secret'; +export type KeyUsage = + | 'decrypt' + | 'deriveBits' + | 'deriveKey' + | 'encrypt' + | 'sign' + | 'unwrapKey' + | 'verify' + | 'wrapKey'; +export interface RsaHashedImportParams extends Algorithm { + hash: HashAlgorithmIdentifier; +} +export interface RsaHashedKeyAlgorithm extends RsaKeyAlgorithm { + hash: KeyAlgorithm; +} +export interface RsaHashedKeyGenParams extends RsaKeyGenParams { + hash: HashAlgorithmIdentifier; +} +export interface RsaKeyAlgorithm extends KeyAlgorithm { + modulusLength: number; + publicExponent: BigInteger; +} +export interface RsaKeyGenParams extends Algorithm { + modulusLength: number; + publicExponent: BigInteger; +} +export interface RsaOaepParams extends Algorithm { + label?: BufferSource; +} +export interface RsaOtherPrimesInfo { + d?: string; + r?: string; + t?: string; +} +export interface RsaPssParams extends Algorithm { + saltLength: number; +} diff --git a/source/crypto.c b/source/crypto.c index b84cfe22..fe766725 100644 --- a/source/crypto.c +++ b/source/crypto.c @@ -1,8 +1,12 @@ #include "crypto.h" #include "async.h" #include "errno.h" +#include +#include #include +static JSClassID nx_crypto_key_class_id; + typedef struct { int err; const char *algorithm; @@ -13,15 +17,96 @@ typedef struct { size_t result_size; } nx_crypto_digest_async_t; +typedef struct { + int err; + + JSValue algorithm_val; + void *algorithm_params; + + JSValue key_val; + nx_crypto_key_t *key; + + JSValue data_val; + void *data; + size_t data_size; + + void *result; + size_t result_size; +} nx_crypto_encrypt_async_t; + +typedef struct { + u8 *iv; +} nx_crypto_aes_cbc_params_t; + +typedef struct { + u64 sector; + size_t sector_size; + bool is_nintendo; +} nx_crypto_aes_xts_params_t; + enum nx_crypto_algorithm { NX_CRYPTO_SHA1, NX_CRYPTO_SHA256, + NX_CRYPTO_SHA384, + NX_CRYPTO_SHA512, }; +// Function to pad the input buffer to a multiple of `block_size` +// (PKCS#7 padding) +void *pad_pkcs7(size_t block_size, const uint8_t *input, size_t input_len, + size_t *out_size) { + size_t padded_len = ((input_len / block_size) + 1) * + block_size; // Next multiple of `block_size` + void *output = malloc(padded_len); + if (output) { + memcpy(output, input, input_len); // Copy original input + uint8_t pad_value = padded_len - input_len; // Padding value + memset(output + input_len, pad_value, pad_value); // Add padding + *out_size = padded_len; + } + return output; +} + +// Function to remove PKCS#7 padding after decryption +size_t unpad_pkcs7(size_t block_size, uint8_t *input, size_t input_len) { + uint8_t pad_value = input[input_len - 1]; + if (pad_value > block_size || pad_value == 0) + return input_len; // Invalid padding + return input_len - pad_value; // Return unpadded length +} + +static void finalizer_crypto_key(JSRuntime *rt, JSValue val) { + nx_crypto_key_t *context = JS_GetOpaque(val, nx_crypto_key_class_id); + if (context) { + JS_FreeValueRT(rt, context->algorithm_cached); + JS_FreeValueRT(rt, context->usages_cached); + if (context->handle) { + js_free_rt(rt, context->handle); + } + js_free_rt(rt, context); + } +} + static void free_array_buffer(JSRuntime *rt, void *opaque, void *ptr) { free(ptr); } +u8 *NX_GetBufferSource(JSContext *ctx, size_t *size, JSValueConst obj) { + if (JS_IsArrayBuffer(obj)) { + return JS_GetArrayBuffer(ctx, size, obj); + } + // Assume it's a typed array + size_t bpe = 0; + size_t offset = 0; + size_t ab_size = 0; + JSValue ab = JS_GetTypedArrayBuffer(ctx, obj, &offset, size, &bpe); + u8 *ptr = JS_GetArrayBuffer(ctx, &ab_size, ab); + if (!ptr) { + return ptr; + } + return ptr + offset; +} + void nx_crypto_digest_do(nx_work_t *req) { nx_crypto_digest_async_t *data = (nx_crypto_digest_async_t *)req->data; enum nx_crypto_algorithm alg = -1; @@ -31,6 +116,12 @@ void nx_crypto_digest_do(nx_work_t *req) { } else if (strcasecmp(data->algorithm, "SHA-256") == 0) { alg = NX_CRYPTO_SHA256; data->result_size = SHA256_HASH_SIZE; + } else if (strcasecmp(data->algorithm, "SHA-384") == 0) { + alg = NX_CRYPTO_SHA384; + data->result_size = 0x30; + } else if (strcasecmp(data->algorithm, "SHA-512") == 0) { + alg = NX_CRYPTO_SHA512; + data->result_size = 0x40; } if (alg == -1) { data->err = ENOTSUP; @@ -48,6 +139,16 @@ void nx_crypto_digest_do(nx_work_t *req) { case NX_CRYPTO_SHA256: sha256CalculateHash(data->result, data->data, data->size); break; + case NX_CRYPTO_SHA384: + case NX_CRYPTO_SHA512: + mbedtls_sha512_context ctx; + mbedtls_sha512_init(&ctx); + mbedtls_sha512_starts( + &ctx, alg == NX_CRYPTO_SHA384); // 0 for SHA-512, 1 for SHA-384 + mbedtls_sha512_update(&ctx, data->data, data->size); + mbedtls_sha512_finish(&ctx, data->result); + mbedtls_sha512_free(&ctx); + break; } } @@ -73,13 +174,197 @@ static JSValue nx_crypto_digest(JSContext *ctx, JSValueConst this_val, int argc, NX_INIT_WORK_T(nx_crypto_digest_async_t); data->algorithm = JS_ToCString(ctx, argv[0]); if (!data->algorithm) { + js_free(ctx, data); + return JS_EXCEPTION; + } + data->data = NX_GetBufferSource(ctx, &data->size, argv[1]); + if (!data->data) { + JS_FreeCString(ctx, data->algorithm); + js_free(ctx, data); return JS_EXCEPTION; } data->data_val = JS_DupValue(ctx, argv[1]); - data->data = JS_GetArrayBuffer(ctx, &data->size, data->data_val); return nx_queue_async(ctx, req, nx_crypto_digest_do, nx_crypto_digest_cb); } +void nx_crypto_encrypt_do(nx_work_t *req) { + nx_crypto_encrypt_async_t *data = (nx_crypto_encrypt_async_t *)req->data; + + if (data->key->algorithm == NX_CRYPTO_KEY_ALGORITHM_AES_CBC) { + nx_crypto_key_aes_t *aes = (nx_crypto_key_aes_t *)data->key->handle; + nx_crypto_aes_cbc_params_t *cbc_params = + (nx_crypto_aes_cbc_params_t *)data->algorithm_params; + + data->result = pad_pkcs7(AES_BLOCK_SIZE, data->data, data->data_size, + &data->result_size); + if (!data->result) { + data->err = ENOMEM; + return; + } + + if (aes->key_length == 16) { + aes128CbcContextResetIv(&aes->encrypt.cbc_128, cbc_params->iv); + aes128CbcEncrypt(&aes->encrypt.cbc_128, data->result, data->result, + data->result_size); + } else if (aes->key_length == 24) { + aes192CbcContextResetIv(&aes->encrypt.cbc_192, cbc_params->iv); + aes192CbcEncrypt(&aes->encrypt.cbc_192, data->result, data->result, + data->result_size); + } else if (aes->key_length == 32) { + aes256CbcContextResetIv(&aes->encrypt.cbc_256, cbc_params->iv); + aes256CbcEncrypt(&aes->encrypt.cbc_256, data->result, data->result, + data->result_size); + } + } else if (data->key->algorithm == NX_CRYPTO_KEY_ALGORITHM_AES_XTS) { + nx_crypto_key_aes_t *aes = (nx_crypto_key_aes_t *)data->key->handle; + nx_crypto_aes_xts_params_t *xts_params = + (nx_crypto_aes_xts_params_t *)data->algorithm_params; + + // In XTS the encrypted size is exactly the plaintext size + data->result = malloc(data->data_size); + if (!data->result) { + data->err = ENOMEM; + return; + } + + if (aes->key_length == 32) { + void *dst = data->result; + void *src = data->data; + u64 sector = xts_params->sector; + for (size_t i = 0; i < data->data_size; + i += xts_params->sector_size) { + aes128XtsContextResetSector(&aes->encrypt.xts_128, sector++, + xts_params->is_nintendo); + data->result_size += aes128XtsEncrypt( + &aes->encrypt.xts_128, dst, src, xts_params->sector_size); + + dst = (u8 *)dst + xts_params->sector_size; + src = (u8 *)src + xts_params->sector_size; + } + } else if (aes->key_length == 48) { + data->err = ENOTSUP; + // aes192XtsContextResetTweak(&aes->encrypt.xts_192, + // xts_params->tweak); + // aes192XtsEncrypt(&aes->encrypt.xts_192, data->result, + // data->data, + // data->data_size); + } else if (aes->key_length == 64) { + data->err = ENOTSUP; + // aes256XtsContextResetTweak(&aes->encrypt.xts_256, + // xts_params->tweak); + // aes256XtsEncrypt(&aes->encrypt.xts_256, data->result, + // data->data, + // data->data_size); + } + } +} + +JSValue nx_crypto_encrypt_cb(JSContext *ctx, nx_work_t *req) { + nx_crypto_encrypt_async_t *data = (nx_crypto_encrypt_async_t *)req->data; + if (data->algorithm_params) { + js_free(ctx, data->algorithm_params); + } + JS_FreeValue(ctx, data->algorithm_val); + JS_FreeValue(ctx, data->key_val); + JS_FreeValue(ctx, data->data_val); + + if (data->err) { + JSValue err = JS_NewError(ctx); + JS_DefinePropertyValueStr(ctx, err, "message", + JS_NewString(ctx, strerror(data->err)), + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + return JS_Throw(ctx, err); + } + + return JS_NewArrayBuffer(ctx, data->result, data->result_size, + free_array_buffer, NULL, false); +} + +static JSValue nx_crypto_encrypt(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { + NX_INIT_WORK_T(nx_crypto_encrypt_async_t); + + data->key = JS_GetOpaque2(ctx, argv[1], nx_crypto_key_class_id); + if (!data->key) { + js_free(ctx, data); + return JS_EXCEPTION; + } + + // Validate that the key may be used for encryption + if (!(data->key->usages & NX_CRYPTO_KEY_USAGE_ENCRYPT)) { + js_free(ctx, data); + return JS_ThrowTypeError( + ctx, "Key does not support the 'encrypt' operation"); + } + + data->data = NX_GetBufferSource(ctx, &data->data_size, argv[2]); + if (!data->data) { + js_free(ctx, data); + return JS_EXCEPTION; + } + + if (data->key->algorithm == NX_CRYPTO_KEY_ALGORITHM_AES_CBC) { + nx_crypto_aes_cbc_params_t *cbc_params = + js_mallocz(ctx, sizeof(nx_crypto_aes_cbc_params_t)); + if (!cbc_params) { + js_free(ctx, data); + return JS_EXCEPTION; + } + + size_t iv_size; + cbc_params->iv = NX_GetBufferSource( + ctx, &iv_size, JS_GetPropertyStr(ctx, argv[0], "iv")); + if (!cbc_params->iv) { + js_free(ctx, data); + js_free(ctx, cbc_params); + return JS_EXCEPTION; + } + + // Validate IV size + if (iv_size != 16) { + js_free(ctx, data); + js_free(ctx, cbc_params); + return JS_ThrowTypeError(ctx, + "Initialization vector must be 16 bytes"); + } + + data->algorithm_params = cbc_params; + } else if (data->key->algorithm == NX_CRYPTO_KEY_ALGORITHM_AES_XTS) { + nx_crypto_aes_xts_params_t *xts_params = + js_mallocz(ctx, sizeof(nx_crypto_aes_xts_params_t)); + if (!xts_params) { + js_free(ctx, data); + return JS_EXCEPTION; + } + + int is_nintendo = + JS_ToBool(ctx, JS_GetPropertyStr(ctx, argv[0], "isNintendo")); + + u32 sector; + u32 sector_size; + if (is_nintendo == -1 || + JS_ToUint32(ctx, §or, + JS_GetPropertyStr(ctx, argv[0], "sector")) || + JS_ToUint32(ctx, §or_size, + JS_GetPropertyStr(ctx, argv[0], "sectorSize"))) { + js_free(ctx, data); + js_free(ctx, xts_params); + return JS_EXCEPTION; + } + xts_params->is_nintendo = is_nintendo; + xts_params->sector = sector; + xts_params->sector_size = sector_size; + + data->algorithm_params = xts_params; + } + + data->algorithm_val = JS_DupValue(ctx, argv[0]); + data->key_val = JS_DupValue(ctx, argv[1]); + data->data_val = JS_DupValue(ctx, argv[2]); + + return nx_queue_async(ctx, req, nx_crypto_encrypt_do, nx_crypto_encrypt_cb); +} + static JSValue nx_crypto_random_bytes(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { size_t size; @@ -118,13 +403,528 @@ static JSValue nx_crypto_sha256_hex(JSContext *ctx, JSValueConst this_val, return hex_val; } +static JSValue nx_crypto_key_get_type(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { + nx_crypto_key_t *context = + JS_GetOpaque2(ctx, this_val, nx_crypto_key_class_id); + if (!context) + return JS_EXCEPTION; + + char *type; + switch (context->type) { + case NX_CRYPTO_KEY_TYPE_UNKNOWN: + type = "unknown"; + case NX_CRYPTO_KEY_TYPE_PRIVATE: + type = "private"; + case NX_CRYPTO_KEY_TYPE_PUBLIC: + type = "public"; + case NX_CRYPTO_KEY_TYPE_SECRET: + type = "secret"; + } + return JS_NewString(ctx, type); +} + +static JSValue nx_crypto_key_get_extractable(JSContext *ctx, + JSValueConst this_val, int argc, + JSValueConst *argv) { + nx_crypto_key_t *context = + JS_GetOpaque2(ctx, this_val, nx_crypto_key_class_id); + if (!context) + return JS_EXCEPTION; + + return JS_NewBool(ctx, context->extractable); +} + +static JSValue nx_crypto_key_get_algorithm(JSContext *ctx, + JSValueConst this_val, int argc, + JSValueConst *argv) { + nx_crypto_key_t *context = + JS_GetOpaque2(ctx, this_val, nx_crypto_key_class_id); + if (!context) + return JS_EXCEPTION; + + if (JS_IsUndefined(context->algorithm_cached)) { + JSValue obj = JS_NewObject(ctx); + + char *name_val = ""; + switch (context->algorithm) { + case NX_CRYPTO_KEY_ALGORITHM_AES_CBC: + name_val = "AES-CBC"; + break; + case NX_CRYPTO_KEY_ALGORITHM_AES_XTS: + name_val = "AES-XTS"; + break; + default: + // TODO: throw error? + break; + } + JS_SetPropertyStr(ctx, obj, "name", JS_NewString(ctx, name_val)); + + if (context->algorithm == NX_CRYPTO_KEY_ALGORITHM_AES_CBC || + context->algorithm == NX_CRYPTO_KEY_ALGORITHM_AES_XTS) { + nx_crypto_key_aes_t *aes = (nx_crypto_key_aes_t *)context->handle; + JS_SetPropertyStr(ctx, obj, "length", + JS_NewUint32(ctx, aes->key_length * 8)); + } + context->algorithm_cached = obj; + } + + return JS_DupValue(ctx, context->algorithm_cached); +} + +static JSValue nx_crypto_key_get_usages(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { + nx_crypto_key_t *context = + JS_GetOpaque2(ctx, this_val, nx_crypto_key_class_id); + if (!context) + return JS_EXCEPTION; + + if (JS_IsUndefined(context->usages_cached)) { + size_t index = 0; + JSValue arr = JS_NewArray(ctx); + + if (context->usages & NX_CRYPTO_KEY_USAGE_DECRYPT) { + JS_SetPropertyUint32(ctx, arr, index++, + JS_NewString(ctx, "decrypt")); + } + if (context->usages & NX_CRYPTO_KEY_USAGE_DERIVE_BITS) { + JS_SetPropertyUint32(ctx, arr, index++, + JS_NewString(ctx, "deriveBits")); + } + if (context->usages & NX_CRYPTO_KEY_USAGE_DERIVE_KEY) { + JS_SetPropertyUint32(ctx, arr, index++, + JS_NewString(ctx, "deriveKey")); + } + if (context->usages & NX_CRYPTO_KEY_USAGE_ENCRYPT) { + JS_SetPropertyUint32(ctx, arr, index++, + JS_NewString(ctx, "encrypt")); + } + if (context->usages & NX_CRYPTO_KEY_USAGE_SIGN) { + JS_SetPropertyUint32(ctx, arr, index++, JS_NewString(ctx, "sign")); + } + if (context->usages & NX_CRYPTO_KEY_USAGE_UNWRAP_KEY) { + JS_SetPropertyUint32(ctx, arr, index++, + JS_NewString(ctx, "unwrapKey")); + } + if (context->usages & NX_CRYPTO_KEY_USAGE_VERIFY) { + JS_SetPropertyUint32(ctx, arr, index++, + JS_NewString(ctx, "verify")); + } + if (context->usages & NX_CRYPTO_KEY_USAGE_WRAP_KEY) { + JS_SetPropertyUint32(ctx, arr, index++, + JS_NewString(ctx, "wrapKey")); + } + + context->usages_cached = arr; + } + + return JS_DupValue(ctx, context->usages_cached); +} + +static JSValue nx_crypto_key_new(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { + nx_crypto_key_t *context = js_mallocz(ctx, sizeof(nx_crypto_key_t)); + if (!context) + return JS_EXCEPTION; + + context->algorithm_cached = JS_UNDEFINED; + context->usages_cached = JS_UNDEFINED; + + size_t key_size; + const void *key_data = NX_GetBufferSource(ctx, &key_size, argv[1]); + if (!key_data) { + js_free(ctx, context); + return JS_EXCEPTION; + } + + int extractable = JS_ToBool(ctx, argv[2]); + if (extractable == -1) { + js_free(ctx, context); + return JS_EXCEPTION; + } + context->extractable = extractable; + + uint32_t usages_size; + if (JS_ToUint32(ctx, &usages_size, + JS_GetPropertyStr(ctx, argv[3], "length"))) { + js_free(ctx, context); + return JS_EXCEPTION; + } + for (uint32_t i = 0; i < usages_size; i++) { + JSValue usage_val = JS_GetPropertyUint32(ctx, argv[3], i); + if (!JS_IsString(usage_val)) { + js_free(ctx, context); + JS_ThrowTypeError(ctx, "Expected string for usage"); + return JS_EXCEPTION; + } + const char *usage = JS_ToCString(ctx, usage_val); + if (!usage) { + js_free(ctx, context); + return JS_EXCEPTION; + } + if (strcmp(usage, "decrypt") == 0) { + context->usages |= NX_CRYPTO_KEY_USAGE_DECRYPT; + } else if (strcmp(usage, "deriveBits") == 0) { + context->usages |= NX_CRYPTO_KEY_USAGE_DERIVE_BITS; + } else if (strcmp(usage, "deriveKey") == 0) { + context->usages |= NX_CRYPTO_KEY_USAGE_DERIVE_KEY; + } else if (strcmp(usage, "encrypt") == 0) { + context->usages |= NX_CRYPTO_KEY_USAGE_ENCRYPT; + } else if (strcmp(usage, "sign") == 0) { + context->usages |= NX_CRYPTO_KEY_USAGE_SIGN; + } else if (strcmp(usage, "unwrapKey") == 0) { + context->usages |= NX_CRYPTO_KEY_USAGE_UNWRAP_KEY; + } else if (strcmp(usage, "verify") == 0) { + context->usages |= NX_CRYPTO_KEY_USAGE_VERIFY; + } else if (strcmp(usage, "wrapKey") == 0) { + context->usages |= NX_CRYPTO_KEY_USAGE_WRAP_KEY; + } else { + js_free(ctx, context); + JS_FreeCString(ctx, usage); + JS_ThrowTypeError(ctx, "Invalid usage"); + return JS_EXCEPTION; + } + JS_FreeCString(ctx, usage); + } + + JSValue algo_val = JS_GetPropertyStr(ctx, argv[0], "name"); + if (!JS_IsString(algo_val)) { + js_free(ctx, context); + JS_ThrowTypeError(ctx, "Expected string for algorithm \"name\""); + return JS_EXCEPTION; + } + const char *algo = JS_ToCString(ctx, algo_val); + if (!algo) { + js_free(ctx, context); + return JS_EXCEPTION; + } + + if (strcmp(algo, "AES-CBC") == 0) { + if (key_size != 16 && key_size != 24 && key_size != 32) { + js_free(ctx, context); + JS_FreeCString(ctx, algo); + JS_ThrowTypeError(ctx, "Invalid key size for AES-CBC"); + return JS_EXCEPTION; + } + context->type = NX_CRYPTO_KEY_TYPE_SECRET; + context->algorithm = NX_CRYPTO_KEY_ALGORITHM_AES_CBC; + + nx_crypto_key_aes_t *aes = js_mallocz(ctx, sizeof(nx_crypto_key_aes_t)); + if (!aes) { + js_free(ctx, context); + JS_FreeCString(ctx, algo); + return JS_EXCEPTION; + } + context->handle = aes; + aes->key_length = key_size; + + if (key_size == 16) { + if (context->usages & NX_CRYPTO_KEY_USAGE_ENCRYPT) { + aes128CbcContextCreate(&aes->encrypt.cbc_128, key_data, + key_data, true); + } + if (context->usages & NX_CRYPTO_KEY_USAGE_DECRYPT) { + aes128CbcContextCreate(&aes->decrypt.cbc_128, key_data, + key_data, false); + } + } else if (key_size == 24) { + if (context->usages & NX_CRYPTO_KEY_USAGE_ENCRYPT) { + aes192CbcContextCreate(&aes->encrypt.cbc_192, key_data, + key_data, true); + } + if (context->usages & NX_CRYPTO_KEY_USAGE_DECRYPT) { + aes192CbcContextCreate(&aes->decrypt.cbc_192, key_data, + key_data, false); + } + } else { + if (context->usages & NX_CRYPTO_KEY_USAGE_ENCRYPT) { + aes256CbcContextCreate(&aes->encrypt.cbc_256, key_data, + key_data, true); + } + if (context->usages & NX_CRYPTO_KEY_USAGE_DECRYPT) { + aes256CbcContextCreate(&aes->decrypt.cbc_256, key_data, + key_data, false); + } + } + } else if (strcmp(algo, "AES-XTS") == 0) { + if (key_size != 32 && key_size != 48 && key_size != 64) { + js_free(ctx, context); + JS_FreeCString(ctx, algo); + JS_ThrowTypeError(ctx, "Invalid key size for AES-XTS"); + return JS_EXCEPTION; + } + context->type = NX_CRYPTO_KEY_TYPE_SECRET; + context->algorithm = NX_CRYPTO_KEY_ALGORITHM_AES_XTS; + + nx_crypto_key_aes_t *aes = js_mallocz(ctx, sizeof(nx_crypto_key_aes_t)); + if (!aes) { + js_free(ctx, context); + JS_FreeCString(ctx, algo); + return JS_EXCEPTION; + } + context->handle = aes; + aes->key_length = key_size; + + if (key_size == 32) { + if (context->usages & NX_CRYPTO_KEY_USAGE_ENCRYPT) { + aes128XtsContextCreate(&aes->encrypt.xts_128, key_data, + key_data + 0x10, true); + } + if (context->usages & NX_CRYPTO_KEY_USAGE_DECRYPT) { + aes128XtsContextCreate(&aes->decrypt.xts_128, key_data, + key_data + 0x10, false); + } + } else if (key_size == 48) { + if (context->usages & NX_CRYPTO_KEY_USAGE_ENCRYPT) { + aes192XtsContextCreate(&aes->encrypt.xts_192, key_data, + key_data + 0x18, true); + } + if (context->usages & NX_CRYPTO_KEY_USAGE_DECRYPT) { + aes192XtsContextCreate(&aes->decrypt.xts_192, key_data, + key_data + 0x18, false); + } + } else { + if (context->usages & NX_CRYPTO_KEY_USAGE_ENCRYPT) { + aes256XtsContextCreate(&aes->encrypt.xts_256, key_data, + key_data + 0x20, true); + } + if (context->usages & NX_CRYPTO_KEY_USAGE_DECRYPT) { + aes256XtsContextCreate(&aes->decrypt.xts_256, key_data, + key_data + 0x20, false); + } + } + } else { + JS_ThrowTypeError(ctx, "Unrecognized algorithm name: \"%s\"", algo); + js_free(ctx, context); + JS_FreeCString(ctx, algo); + return JS_EXCEPTION; + } + + JS_FreeCString(ctx, algo); + + JSValue obj = JS_NewObjectClass(ctx, nx_crypto_key_class_id); + if (JS_IsException(obj)) { + if (context->handle) { + js_free(ctx, context->handle); + } + js_free(ctx, context); + return obj; + } + + JS_SetOpaque(obj, context); + return obj; +} + +static JSValue nx_crypto_key_init(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { + JSAtom atom; + JSValue proto = JS_GetPropertyStr(ctx, argv[0], "prototype"); + NX_DEF_GET(proto, "type", nx_crypto_key_get_type); + NX_DEF_GET(proto, "extractable", nx_crypto_key_get_extractable); + NX_DEF_GET(proto, "algorithm", nx_crypto_key_get_algorithm); + NX_DEF_GET(proto, "usages", nx_crypto_key_get_usages); + JS_FreeValue(ctx, proto); + return JS_UNDEFINED; +} + +void nx_crypto_decrypt_do(nx_work_t *req) { + nx_crypto_encrypt_async_t *data = (nx_crypto_encrypt_async_t *)req->data; + + if (data->key->algorithm == NX_CRYPTO_KEY_ALGORITHM_AES_CBC) { + nx_crypto_key_aes_t *aes = (nx_crypto_key_aes_t *)data->key->handle; + nx_crypto_aes_cbc_params_t *cbc_params = + (nx_crypto_aes_cbc_params_t *)data->algorithm_params; + + data->result = calloc(data->data_size, 1); + if (!data->result) { + data->err = ENOMEM; + return; + } + + if (aes->key_length == 16) { + aes128CbcContextResetIv(&aes->decrypt.cbc_128, cbc_params->iv); + aes128CbcDecrypt(&aes->decrypt.cbc_128, data->result, data->data, + data->data_size); + } else if (aes->key_length == 24) { + aes192CbcContextResetIv(&aes->decrypt.cbc_192, cbc_params->iv); + aes192CbcDecrypt(&aes->decrypt.cbc_192, data->result, data->data, + data->data_size); + } else if (aes->key_length == 32) { + aes256CbcContextResetIv(&aes->decrypt.cbc_256, cbc_params->iv); + aes256CbcDecrypt(&aes->decrypt.cbc_256, data->result, data->data, + data->data_size); + } + + data->result_size = + unpad_pkcs7(AES_BLOCK_SIZE, data->result, data->data_size); + } else if (data->key->algorithm == NX_CRYPTO_KEY_ALGORITHM_AES_XTS) { + nx_crypto_key_aes_t *aes = (nx_crypto_key_aes_t *)data->key->handle; + nx_crypto_aes_xts_params_t *xts_params = + (nx_crypto_aes_xts_params_t *)data->algorithm_params; + + // In XTS the decrypted size is exactly the ciphertext size + data->result = malloc(data->data_size); + if (!data->result) { + data->err = ENOMEM; + return; + } + + if (aes->key_length == 32) { + void *dst = data->result; + void *src = data->data; + uint64_t sector = xts_params->sector; + for (size_t i = 0; i < data->data_size; + i += xts_params->sector_size) { + aes128XtsContextResetSector(&aes->decrypt.xts_128, sector++, + xts_params->is_nintendo); + data->result_size += aes128XtsDecrypt( + &aes->decrypt.xts_128, dst, src, xts_params->sector_size); + + dst = (u8 *)dst + xts_params->sector_size; + src = (u8 *)src + xts_params->sector_size; + } + } else if (aes->key_length == 48) { + data->err = ENOTSUP; + } else if (aes->key_length == 64) { + data->err = ENOTSUP; + } + } +} + +JSValue nx_crypto_decrypt_cb(JSContext *ctx, nx_work_t *req) { + nx_crypto_encrypt_async_t *data = (nx_crypto_encrypt_async_t *)req->data; + if (data->algorithm_params) { + js_free(ctx, data->algorithm_params); + } + JS_FreeValue(ctx, data->algorithm_val); + JS_FreeValue(ctx, data->key_val); + JS_FreeValue(ctx, data->data_val); + + if (data->err) { + JSValue err = JS_NewError(ctx); + JS_DefinePropertyValueStr(ctx, err, "message", + JS_NewString(ctx, strerror(data->err)), + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + return JS_Throw(ctx, err); + } + + return JS_NewArrayBuffer(ctx, data->result, data->result_size, + free_array_buffer, NULL, false); +} + +static JSValue nx_crypto_subtle_decrypt(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { + NX_INIT_WORK_T(nx_crypto_encrypt_async_t); + + data->key = JS_GetOpaque2(ctx, argv[1], nx_crypto_key_class_id); + if (!data->key) { + js_free(ctx, data); + return JS_EXCEPTION; + } + + // Validate that the key may be used for decryption + if (!(data->key->usages & NX_CRYPTO_KEY_USAGE_DECRYPT)) { + js_free(ctx, data); + return JS_ThrowTypeError( + ctx, "Key does not support the 'decrypt' operation"); + } + + data->data = NX_GetBufferSource(ctx, &data->data_size, argv[2]); + if (!data->data) { + js_free(ctx, data); + return JS_EXCEPTION; + } + + if (data->key->algorithm == NX_CRYPTO_KEY_ALGORITHM_AES_CBC) { + nx_crypto_aes_cbc_params_t *cbc_params = + js_mallocz(ctx, sizeof(nx_crypto_aes_cbc_params_t)); + if (!cbc_params) { + js_free(ctx, data); + return JS_EXCEPTION; + } + + size_t iv_size; + cbc_params->iv = NX_GetBufferSource( + ctx, &iv_size, JS_GetPropertyStr(ctx, argv[0], "iv")); + if (!cbc_params->iv) { + js_free(ctx, data); + js_free(ctx, cbc_params); + return JS_EXCEPTION; + } + + // Validate IV size + if (iv_size != 16) { + js_free(ctx, data); + js_free(ctx, cbc_params); + return JS_ThrowTypeError(ctx, + "Initialization vector must be 16 bytes"); + } + + data->algorithm_params = cbc_params; + } else if (data->key->algorithm == NX_CRYPTO_KEY_ALGORITHM_AES_XTS) { + nx_crypto_aes_xts_params_t *xts_params = + js_mallocz(ctx, sizeof(nx_crypto_aes_xts_params_t)); + if (!xts_params) { + js_free(ctx, data); + return JS_EXCEPTION; + } + + int is_nintendo = + JS_ToBool(ctx, JS_GetPropertyStr(ctx, argv[0], "isNintendo")); + + u32 sector; + u32 sector_size; + if (is_nintendo == -1 || + JS_ToUint32(ctx, §or, + JS_GetPropertyStr(ctx, argv[0], "sector")) || + JS_ToUint32(ctx, §or_size, + JS_GetPropertyStr(ctx, argv[0], "sectorSize"))) { + js_free(ctx, data); + js_free(ctx, xts_params); + return JS_EXCEPTION; + } + xts_params->is_nintendo = is_nintendo; + xts_params->sector = sector; + xts_params->sector_size = sector_size; + + data->algorithm_params = xts_params; + } + + data->algorithm_val = JS_DupValue(ctx, argv[0]); + data->key_val = JS_DupValue(ctx, argv[1]); + data->data_val = JS_DupValue(ctx, argv[2]); + + return nx_queue_async(ctx, req, nx_crypto_decrypt_do, nx_crypto_decrypt_cb); +} + +static JSValue nx_crypto_subtle_init(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { + JSValue proto = JS_GetPropertyStr(ctx, argv[0], "prototype"); + NX_DEF_FUNC(proto, "decrypt", nx_crypto_subtle_decrypt, 3); + JS_FreeValue(ctx, proto); + return JS_UNDEFINED; +} + static const JSCFunctionListEntry function_list[] = { + JS_CFUNC_DEF("cryptoKeyNew", 1, nx_crypto_key_new), + JS_CFUNC_DEF("cryptoKeyInit", 1, nx_crypto_key_init), + JS_CFUNC_DEF("cryptoSubtleInit", 1, nx_crypto_subtle_init), JS_CFUNC_DEF("cryptoDigest", 0, nx_crypto_digest), + JS_CFUNC_DEF("cryptoEncrypt", 0, nx_crypto_encrypt), JS_CFUNC_DEF("cryptoRandomBytes", 0, nx_crypto_random_bytes), JS_CFUNC_DEF("sha256Hex", 0, nx_crypto_sha256_hex), }; void nx_init_crypto(JSContext *ctx, JSValueConst init_obj) { + JSRuntime *rt = JS_GetRuntime(ctx); + + JS_NewClassID(rt, &nx_crypto_key_class_id); + JSClassDef crypto_key_class = { + "CryptoKey", + .finalizer = finalizer_crypto_key, + }; + JS_NewClass(rt, nx_crypto_key_class_id, &crypto_key_class); + JS_SetPropertyFunctionList(ctx, init_obj, function_list, countof(function_list)); } diff --git a/source/crypto.h b/source/crypto.h index da0fd85b..d1671e37 100644 --- a/source/crypto.h +++ b/source/crypto.h @@ -1,4 +1,60 @@ #pragma once #include "types.h" +typedef enum { + NX_CRYPTO_KEY_TYPE_UNKNOWN, + NX_CRYPTO_KEY_TYPE_PRIVATE, + NX_CRYPTO_KEY_TYPE_PUBLIC, + NX_CRYPTO_KEY_TYPE_SECRET +} nx_crypto_key_type; + +typedef enum { + NX_CRYPTO_KEY_ALGORITHM_UNKNOWN, + NX_CRYPTO_KEY_ALGORITHM_AES_CBC, + NX_CRYPTO_KEY_ALGORITHM_AES_XTS +} nx_crypto_key_algorithm; + +typedef enum { + NX_CRYPTO_KEY_USAGE_DECRYPT = BIT(0), + NX_CRYPTO_KEY_USAGE_DERIVE_BITS = BIT(1), + NX_CRYPTO_KEY_USAGE_DERIVE_KEY = BIT(2), + NX_CRYPTO_KEY_USAGE_ENCRYPT = BIT(3), + NX_CRYPTO_KEY_USAGE_SIGN = BIT(4), + NX_CRYPTO_KEY_USAGE_UNWRAP_KEY = BIT(5), + NX_CRYPTO_KEY_USAGE_VERIFY = BIT(6), + NX_CRYPTO_KEY_USAGE_WRAP_KEY = BIT(7) +} nx_crypto_key_usage; + +typedef struct { + nx_crypto_key_type type; + bool extractable; + nx_crypto_key_algorithm algorithm; + JSValue algorithm_cached; + nx_crypto_key_usage usages; + JSValue usages_cached; + void *handle; +} nx_crypto_key_t; + +typedef struct { + u8 key_length; /* 16 (128-bit), 24 (192-bit), or 32 (256-bit) */ + union { + Aes128CbcContext cbc_128; + Aes192CbcContext cbc_192; + Aes256CbcContext cbc_256; + + Aes128XtsContext xts_128; + Aes192XtsContext xts_192; + Aes256XtsContext xts_256; + } decrypt; + union { + Aes128CbcContext cbc_128; + Aes192CbcContext cbc_192; + Aes256CbcContext cbc_256; + + Aes128XtsContext xts_128; + Aes192XtsContext xts_192; + Aes256XtsContext xts_256; + } encrypt; +} nx_crypto_key_aes_t; + void nx_init_crypto(JSContext *ctx, JSValueConst init_obj);