@tootallnate/aes-xts 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4 @@
1
+
2
+ > @tootallnate/aes-xts@0.0.0 build /Users/nrajlich/Code/TooTallNate/switch-tools/packages/aes-xts
3
+ > tsc
4
+
@@ -0,0 +1,32 @@
1
+ /**
2
+ * AES-128-XTS encryption/decryption using Web Crypto.
3
+ *
4
+ * Supports Nintendo's big-endian tweak format, where the sector number
5
+ * is stored as a big-endian 128-bit value (opposite of the IEEE P1619
6
+ * standard which uses little-endian).
7
+ *
8
+ * Implementation uses Web Crypto's AES-CBC with a zero IV to perform
9
+ * single-block AES-ECB operations, then builds the XTS mode on top.
10
+ */
11
+ /**
12
+ * Encrypt data using AES-128-XTS with Nintendo's big-endian tweak.
13
+ *
14
+ * @param key - 32-byte key (first 16 bytes = data key K1, last 16 bytes = tweak key K2)
15
+ * @param data - Data to encrypt (must be a multiple of `sectorSize`)
16
+ * @param sectorSize - Size of each sector in bytes (typically 0x200 for NCA headers)
17
+ * @param startSector - Starting sector number (default: 0)
18
+ * @param crypto - Optional Crypto implementation (defaults to globalThis.crypto)
19
+ * @returns Encrypted data
20
+ */
21
+ export declare function encrypt(key: ArrayBuffer | Uint8Array, data: ArrayBuffer | Uint8Array, sectorSize: number, startSector?: number, crypto?: Crypto): Promise<ArrayBuffer>;
22
+ /**
23
+ * Decrypt data using AES-128-XTS with Nintendo's big-endian tweak.
24
+ *
25
+ * @param key - 32-byte key (first 16 bytes = data key K1, last 16 bytes = tweak key K2)
26
+ * @param data - Data to decrypt (must be a multiple of `sectorSize`)
27
+ * @param sectorSize - Size of each sector in bytes (typically 0x200 for NCA headers)
28
+ * @param startSector - Starting sector number (default: 0)
29
+ * @param crypto - Optional Crypto implementation (defaults to globalThis.crypto)
30
+ * @returns Decrypted data
31
+ */
32
+ export declare function decrypt(key: ArrayBuffer | Uint8Array, data: ArrayBuffer | Uint8Array, sectorSize: number, startSector?: number, crypto?: Crypto): Promise<ArrayBuffer>;
package/dist/index.js ADDED
@@ -0,0 +1,211 @@
1
+ /**
2
+ * AES-128-XTS encryption/decryption using Web Crypto.
3
+ *
4
+ * Supports Nintendo's big-endian tweak format, where the sector number
5
+ * is stored as a big-endian 128-bit value (opposite of the IEEE P1619
6
+ * standard which uses little-endian).
7
+ *
8
+ * Implementation uses Web Crypto's AES-CBC with a zero IV to perform
9
+ * single-block AES-ECB operations, then builds the XTS mode on top.
10
+ */
11
+ const BLOCK_SIZE = 16;
12
+ const ZERO_IV = new Uint8Array(BLOCK_SIZE);
13
+ /**
14
+ * Import a raw AES-128 key for use with AES-CBC (used to emulate ECB).
15
+ */
16
+ async function importKey(rawKey, crypto = globalThis.crypto) {
17
+ return crypto.subtle.importKey('raw', rawKey, { name: 'AES-CBC' }, false, [
18
+ 'encrypt',
19
+ 'decrypt',
20
+ ]);
21
+ }
22
+ /**
23
+ * Encrypt a single 16-byte block using AES-ECB (emulated via AES-CBC with zero IV).
24
+ * AES-CBC with a zero IV and a single block is equivalent to AES-ECB for that block.
25
+ * Web Crypto's AES-CBC adds PKCS7 padding, so the output is 32 bytes — we take only the first 16.
26
+ */
27
+ async function ecbEncryptBlock(key, block, crypto = globalThis.crypto) {
28
+ const result = await crypto.subtle.encrypt({ name: 'AES-CBC', iv: ZERO_IV }, key, block);
29
+ return new Uint8Array(result, 0, BLOCK_SIZE);
30
+ }
31
+ /**
32
+ * Decrypt a single 16-byte block using AES-ECB (emulated via AES-CBC with zero IV).
33
+ *
34
+ * Strategy: Construct a 2-block CBC ciphertext [C0, C1] such that when decrypted
35
+ * with IV=0, the first plaintext block P0 = AES-ECB-Decrypt(C0) and the second
36
+ * block P1 has valid PKCS7 padding (16 bytes of 0x10).
37
+ *
38
+ * In CBC decrypt: P0 = AES-Decrypt(C0) XOR IV = AES-Decrypt(C0) (since IV=0)
39
+ * P1 = AES-Decrypt(C1) XOR C0
40
+ *
41
+ * For P1 to be valid PKCS7 padding: we need AES-Decrypt(C1) = padding XOR C0
42
+ * So C1 = AES-Encrypt(padding XOR C0) = AES-Encrypt(padding XOR block)
43
+ */
44
+ async function ecbDecryptBlock(key, block, crypto = globalThis.crypto) {
45
+ // PKCS7 padding for a full block: 16 bytes of 0x10
46
+ const padding = new Uint8Array(BLOCK_SIZE);
47
+ padding.fill(0x10);
48
+ // XOR padding with the ciphertext block (C0)
49
+ const paddingXorBlock = new Uint8Array(BLOCK_SIZE);
50
+ for (let i = 0; i < BLOCK_SIZE; i++) {
51
+ paddingXorBlock[i] = padding[i] ^ block[i];
52
+ }
53
+ // C1 = AES-ECB-Encrypt(padding XOR block)
54
+ const c1 = await ecbEncryptBlock(key, paddingXorBlock, crypto);
55
+ // Construct [C0=block, C1] and CBC-decrypt with IV=0
56
+ const cbcCiphertext = new Uint8Array(BLOCK_SIZE * 2);
57
+ cbcCiphertext.set(block, 0);
58
+ cbcCiphertext.set(c1, BLOCK_SIZE);
59
+ const result = await crypto.subtle.decrypt({ name: 'AES-CBC', iv: ZERO_IV }, key, cbcCiphertext);
60
+ return new Uint8Array(result, 0, BLOCK_SIZE);
61
+ }
62
+ /**
63
+ * Multiply a value in GF(2^128) by the primitive element alpha (x).
64
+ * This is the standard XTS doubling operation.
65
+ *
66
+ * The multiplication is performed on the 16-byte value interpreted as a
67
+ * little-endian polynomial in GF(2^128) with the irreducible polynomial
68
+ * x^128 + x^7 + x^2 + x + 1 (0x87 reduction).
69
+ *
70
+ * Operates in-place on the provided buffer.
71
+ */
72
+ function gf128Mul(block) {
73
+ let carry = 0;
74
+ for (let i = 0; i < BLOCK_SIZE; i++) {
75
+ const nextCarry = (block[i] >> 7) & 1;
76
+ block[i] = ((block[i] << 1) | carry) & 0xff;
77
+ carry = nextCarry;
78
+ }
79
+ // If there was a carry out, XOR with the reduction polynomial
80
+ if (carry) {
81
+ block[0] ^= 0x87;
82
+ }
83
+ }
84
+ /**
85
+ * XOR two 16-byte blocks, storing the result in `dst`.
86
+ */
87
+ function xorBlocks(dst, a, b) {
88
+ for (let i = 0; i < BLOCK_SIZE; i++) {
89
+ dst[i] = a[i] ^ b[i];
90
+ }
91
+ }
92
+ /**
93
+ * Generate the Nintendo big-endian tweak value for a given sector number.
94
+ *
95
+ * Nintendo stores the sector number as a big-endian 128-bit value,
96
+ * which is the opposite of the IEEE P1619 standard (little-endian).
97
+ *
98
+ * This matches the C implementation:
99
+ * ```c
100
+ * static void get_tweak(unsigned char *tweak, size_t sector) {
101
+ * for (int i = 0xF; i >= 0; i--) {
102
+ * tweak[i] = (unsigned char)(sector & 0xFF);
103
+ * sector >>= 8;
104
+ * }
105
+ * }
106
+ * ```
107
+ */
108
+ function getNintendoTweak(sector) {
109
+ const tweak = new Uint8Array(BLOCK_SIZE);
110
+ let s = sector;
111
+ for (let i = 0xf; i >= 0; i--) {
112
+ tweak[i] = s & 0xff;
113
+ s = Math.floor(s / 256); // Avoid issues with bitwise ops on large numbers
114
+ }
115
+ return tweak;
116
+ }
117
+ /**
118
+ * Encrypt data using AES-128-XTS with Nintendo's big-endian tweak.
119
+ *
120
+ * @param key - 32-byte key (first 16 bytes = data key K1, last 16 bytes = tweak key K2)
121
+ * @param data - Data to encrypt (must be a multiple of `sectorSize`)
122
+ * @param sectorSize - Size of each sector in bytes (typically 0x200 for NCA headers)
123
+ * @param startSector - Starting sector number (default: 0)
124
+ * @param crypto - Optional Crypto implementation (defaults to globalThis.crypto)
125
+ * @returns Encrypted data
126
+ */
127
+ export async function encrypt(key, data, sectorSize, startSector = 0, crypto = globalThis.crypto) {
128
+ const keyBytes = new Uint8Array(key);
129
+ if (keyBytes.length !== 32) {
130
+ throw new Error(`AES-XTS key must be 32 bytes, got ${keyBytes.length}`);
131
+ }
132
+ const dataBytes = new Uint8Array(data);
133
+ if (dataBytes.length % sectorSize !== 0) {
134
+ throw new Error('Data length must be a multiple of sector size');
135
+ }
136
+ if (sectorSize % BLOCK_SIZE !== 0) {
137
+ throw new Error('Sector size must be a multiple of 16');
138
+ }
139
+ const k1 = await importKey(keyBytes.subarray(0, 16), crypto);
140
+ const k2 = await importKey(keyBytes.subarray(16, 32), crypto);
141
+ const output = new Uint8Array(dataBytes.length);
142
+ const blocksPerSector = sectorSize / BLOCK_SIZE;
143
+ const tempBlock = new Uint8Array(BLOCK_SIZE);
144
+ for (let sectorOffset = 0; sectorOffset < dataBytes.length; sectorOffset += sectorSize) {
145
+ const sector = startSector + sectorOffset / sectorSize;
146
+ // Compute the initial tweak: T = AES-ECB-Encrypt(K2, tweak_value)
147
+ const tweakValue = getNintendoTweak(sector);
148
+ const T = await ecbEncryptBlock(k2, tweakValue, crypto);
149
+ for (let j = 0; j < blocksPerSector; j++) {
150
+ const blockOffset = sectorOffset + j * BLOCK_SIZE;
151
+ const plainBlock = dataBytes.subarray(blockOffset, blockOffset + BLOCK_SIZE);
152
+ // PP = P XOR T
153
+ xorBlocks(tempBlock, plainBlock, T);
154
+ // CC = AES-ECB-Encrypt(K1, PP)
155
+ const encrypted = await ecbEncryptBlock(k1, tempBlock, crypto);
156
+ // C = CC XOR T
157
+ xorBlocks(output.subarray(blockOffset, blockOffset + BLOCK_SIZE), encrypted, T);
158
+ // T = T * alpha in GF(2^128)
159
+ gf128Mul(T);
160
+ }
161
+ }
162
+ return output.buffer;
163
+ }
164
+ /**
165
+ * Decrypt data using AES-128-XTS with Nintendo's big-endian tweak.
166
+ *
167
+ * @param key - 32-byte key (first 16 bytes = data key K1, last 16 bytes = tweak key K2)
168
+ * @param data - Data to decrypt (must be a multiple of `sectorSize`)
169
+ * @param sectorSize - Size of each sector in bytes (typically 0x200 for NCA headers)
170
+ * @param startSector - Starting sector number (default: 0)
171
+ * @param crypto - Optional Crypto implementation (defaults to globalThis.crypto)
172
+ * @returns Decrypted data
173
+ */
174
+ export async function decrypt(key, data, sectorSize, startSector = 0, crypto = globalThis.crypto) {
175
+ const keyBytes = new Uint8Array(key);
176
+ if (keyBytes.length !== 32) {
177
+ throw new Error(`AES-XTS key must be 32 bytes, got ${keyBytes.length}`);
178
+ }
179
+ const dataBytes = new Uint8Array(data);
180
+ if (dataBytes.length % sectorSize !== 0) {
181
+ throw new Error('Data length must be a multiple of sector size');
182
+ }
183
+ if (sectorSize % BLOCK_SIZE !== 0) {
184
+ throw new Error('Sector size must be a multiple of 16');
185
+ }
186
+ const k1 = await importKey(keyBytes.subarray(0, 16), crypto);
187
+ const k2 = await importKey(keyBytes.subarray(16, 32), crypto);
188
+ const output = new Uint8Array(dataBytes.length);
189
+ const blocksPerSector = sectorSize / BLOCK_SIZE;
190
+ const tempBlock = new Uint8Array(BLOCK_SIZE);
191
+ for (let sectorOffset = 0; sectorOffset < dataBytes.length; sectorOffset += sectorSize) {
192
+ const sector = startSector + sectorOffset / sectorSize;
193
+ // Compute the initial tweak: T = AES-ECB-Encrypt(K2, tweak_value)
194
+ const tweakValue = getNintendoTweak(sector);
195
+ const T = await ecbEncryptBlock(k2, tweakValue, crypto);
196
+ for (let j = 0; j < blocksPerSector; j++) {
197
+ const blockOffset = sectorOffset + j * BLOCK_SIZE;
198
+ const cipherBlock = dataBytes.subarray(blockOffset, blockOffset + BLOCK_SIZE);
199
+ // CC = C XOR T
200
+ xorBlocks(tempBlock, cipherBlock, T);
201
+ // PP = AES-ECB-Decrypt(K1, CC)
202
+ const decrypted = await ecbDecryptBlock(k1, tempBlock, crypto);
203
+ // P = PP XOR T
204
+ xorBlocks(output.subarray(blockOffset, blockOffset + BLOCK_SIZE), decrypted, T);
205
+ // T = T * alpha in GF(2^128)
206
+ gf128Mul(T);
207
+ }
208
+ }
209
+ return output.buffer;
210
+ }
211
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@tootallnate/aes-xts",
3
+ "version": "0.0.0",
4
+ "type": "module",
5
+ "description": "AES-128-XTS encryption/decryption using Web Crypto, with support for Nintendo's big-endian tweak",
6
+ "main": "dist/index.js",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "test": "vitest run"
10
+ },
11
+ "keywords": [],
12
+ "author": "Nathan Rajlich <n@n8.io>",
13
+ "license": "MIT",
14
+ "devDependencies": {
15
+ "typescript": "^5.3.3"
16
+ }
17
+ }
package/src/index.ts ADDED
@@ -0,0 +1,298 @@
1
+ /**
2
+ * AES-128-XTS encryption/decryption using Web Crypto.
3
+ *
4
+ * Supports Nintendo's big-endian tweak format, where the sector number
5
+ * is stored as a big-endian 128-bit value (opposite of the IEEE P1619
6
+ * standard which uses little-endian).
7
+ *
8
+ * Implementation uses Web Crypto's AES-CBC with a zero IV to perform
9
+ * single-block AES-ECB operations, then builds the XTS mode on top.
10
+ */
11
+
12
+ const BLOCK_SIZE = 16;
13
+ const ZERO_IV = new Uint8Array(BLOCK_SIZE);
14
+
15
+ /**
16
+ * Import a raw AES-128 key for use with AES-CBC (used to emulate ECB).
17
+ */
18
+ async function importKey(
19
+ rawKey: ArrayBuffer | Uint8Array,
20
+ crypto: Crypto = globalThis.crypto
21
+ ): Promise<CryptoKey> {
22
+ return crypto.subtle.importKey('raw', rawKey, { name: 'AES-CBC' }, false, [
23
+ 'encrypt',
24
+ 'decrypt',
25
+ ]);
26
+ }
27
+
28
+ /**
29
+ * Encrypt a single 16-byte block using AES-ECB (emulated via AES-CBC with zero IV).
30
+ * AES-CBC with a zero IV and a single block is equivalent to AES-ECB for that block.
31
+ * Web Crypto's AES-CBC adds PKCS7 padding, so the output is 32 bytes — we take only the first 16.
32
+ */
33
+ async function ecbEncryptBlock(
34
+ key: CryptoKey,
35
+ block: Uint8Array,
36
+ crypto: Crypto = globalThis.crypto
37
+ ): Promise<Uint8Array> {
38
+ const result = await crypto.subtle.encrypt(
39
+ { name: 'AES-CBC', iv: ZERO_IV },
40
+ key,
41
+ block
42
+ );
43
+ return new Uint8Array(result, 0, BLOCK_SIZE);
44
+ }
45
+
46
+ /**
47
+ * Decrypt a single 16-byte block using AES-ECB (emulated via AES-CBC with zero IV).
48
+ *
49
+ * Strategy: Construct a 2-block CBC ciphertext [C0, C1] such that when decrypted
50
+ * with IV=0, the first plaintext block P0 = AES-ECB-Decrypt(C0) and the second
51
+ * block P1 has valid PKCS7 padding (16 bytes of 0x10).
52
+ *
53
+ * In CBC decrypt: P0 = AES-Decrypt(C0) XOR IV = AES-Decrypt(C0) (since IV=0)
54
+ * P1 = AES-Decrypt(C1) XOR C0
55
+ *
56
+ * For P1 to be valid PKCS7 padding: we need AES-Decrypt(C1) = padding XOR C0
57
+ * So C1 = AES-Encrypt(padding XOR C0) = AES-Encrypt(padding XOR block)
58
+ */
59
+ async function ecbDecryptBlock(
60
+ key: CryptoKey,
61
+ block: Uint8Array,
62
+ crypto: Crypto = globalThis.crypto
63
+ ): Promise<Uint8Array> {
64
+ // PKCS7 padding for a full block: 16 bytes of 0x10
65
+ const padding = new Uint8Array(BLOCK_SIZE);
66
+ padding.fill(0x10);
67
+
68
+ // XOR padding with the ciphertext block (C0)
69
+ const paddingXorBlock = new Uint8Array(BLOCK_SIZE);
70
+ for (let i = 0; i < BLOCK_SIZE; i++) {
71
+ paddingXorBlock[i] = padding[i] ^ block[i];
72
+ }
73
+
74
+ // C1 = AES-ECB-Encrypt(padding XOR block)
75
+ const c1 = await ecbEncryptBlock(key, paddingXorBlock, crypto);
76
+
77
+ // Construct [C0=block, C1] and CBC-decrypt with IV=0
78
+ const cbcCiphertext = new Uint8Array(BLOCK_SIZE * 2);
79
+ cbcCiphertext.set(block, 0);
80
+ cbcCiphertext.set(c1, BLOCK_SIZE);
81
+
82
+ const result = await crypto.subtle.decrypt(
83
+ { name: 'AES-CBC', iv: ZERO_IV },
84
+ key,
85
+ cbcCiphertext
86
+ );
87
+ return new Uint8Array(result, 0, BLOCK_SIZE);
88
+ }
89
+
90
+ /**
91
+ * Multiply a value in GF(2^128) by the primitive element alpha (x).
92
+ * This is the standard XTS doubling operation.
93
+ *
94
+ * The multiplication is performed on the 16-byte value interpreted as a
95
+ * little-endian polynomial in GF(2^128) with the irreducible polynomial
96
+ * x^128 + x^7 + x^2 + x + 1 (0x87 reduction).
97
+ *
98
+ * Operates in-place on the provided buffer.
99
+ */
100
+ function gf128Mul(block: Uint8Array): void {
101
+ let carry = 0;
102
+ for (let i = 0; i < BLOCK_SIZE; i++) {
103
+ const nextCarry = (block[i] >> 7) & 1;
104
+ block[i] = ((block[i] << 1) | carry) & 0xff;
105
+ carry = nextCarry;
106
+ }
107
+ // If there was a carry out, XOR with the reduction polynomial
108
+ if (carry) {
109
+ block[0] ^= 0x87;
110
+ }
111
+ }
112
+
113
+ /**
114
+ * XOR two 16-byte blocks, storing the result in `dst`.
115
+ */
116
+ function xorBlocks(dst: Uint8Array, a: Uint8Array, b: Uint8Array): void {
117
+ for (let i = 0; i < BLOCK_SIZE; i++) {
118
+ dst[i] = a[i] ^ b[i];
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Generate the Nintendo big-endian tweak value for a given sector number.
124
+ *
125
+ * Nintendo stores the sector number as a big-endian 128-bit value,
126
+ * which is the opposite of the IEEE P1619 standard (little-endian).
127
+ *
128
+ * This matches the C implementation:
129
+ * ```c
130
+ * static void get_tweak(unsigned char *tweak, size_t sector) {
131
+ * for (int i = 0xF; i >= 0; i--) {
132
+ * tweak[i] = (unsigned char)(sector & 0xFF);
133
+ * sector >>= 8;
134
+ * }
135
+ * }
136
+ * ```
137
+ */
138
+ function getNintendoTweak(sector: number): Uint8Array {
139
+ const tweak = new Uint8Array(BLOCK_SIZE);
140
+ let s = sector;
141
+ for (let i = 0xf; i >= 0; i--) {
142
+ tweak[i] = s & 0xff;
143
+ s = Math.floor(s / 256); // Avoid issues with bitwise ops on large numbers
144
+ }
145
+ return tweak;
146
+ }
147
+
148
+ /**
149
+ * Encrypt data using AES-128-XTS with Nintendo's big-endian tweak.
150
+ *
151
+ * @param key - 32-byte key (first 16 bytes = data key K1, last 16 bytes = tweak key K2)
152
+ * @param data - Data to encrypt (must be a multiple of `sectorSize`)
153
+ * @param sectorSize - Size of each sector in bytes (typically 0x200 for NCA headers)
154
+ * @param startSector - Starting sector number (default: 0)
155
+ * @param crypto - Optional Crypto implementation (defaults to globalThis.crypto)
156
+ * @returns Encrypted data
157
+ */
158
+ export async function encrypt(
159
+ key: ArrayBuffer | Uint8Array,
160
+ data: ArrayBuffer | Uint8Array,
161
+ sectorSize: number,
162
+ startSector = 0,
163
+ crypto: Crypto = globalThis.crypto
164
+ ): Promise<ArrayBuffer> {
165
+ const keyBytes = new Uint8Array(key);
166
+ if (keyBytes.length !== 32) {
167
+ throw new Error(`AES-XTS key must be 32 bytes, got ${keyBytes.length}`);
168
+ }
169
+
170
+ const dataBytes = new Uint8Array(data);
171
+ if (dataBytes.length % sectorSize !== 0) {
172
+ throw new Error('Data length must be a multiple of sector size');
173
+ }
174
+ if (sectorSize % BLOCK_SIZE !== 0) {
175
+ throw new Error('Sector size must be a multiple of 16');
176
+ }
177
+
178
+ const k1 = await importKey(keyBytes.subarray(0, 16), crypto);
179
+ const k2 = await importKey(keyBytes.subarray(16, 32), crypto);
180
+
181
+ const output = new Uint8Array(dataBytes.length);
182
+ const blocksPerSector = sectorSize / BLOCK_SIZE;
183
+ const tempBlock = new Uint8Array(BLOCK_SIZE);
184
+
185
+ for (
186
+ let sectorOffset = 0;
187
+ sectorOffset < dataBytes.length;
188
+ sectorOffset += sectorSize
189
+ ) {
190
+ const sector = startSector + sectorOffset / sectorSize;
191
+
192
+ // Compute the initial tweak: T = AES-ECB-Encrypt(K2, tweak_value)
193
+ const tweakValue = getNintendoTweak(sector);
194
+ const T = await ecbEncryptBlock(k2, tweakValue, crypto);
195
+
196
+ for (let j = 0; j < blocksPerSector; j++) {
197
+ const blockOffset = sectorOffset + j * BLOCK_SIZE;
198
+ const plainBlock = dataBytes.subarray(
199
+ blockOffset,
200
+ blockOffset + BLOCK_SIZE
201
+ );
202
+
203
+ // PP = P XOR T
204
+ xorBlocks(tempBlock, plainBlock, T);
205
+
206
+ // CC = AES-ECB-Encrypt(K1, PP)
207
+ const encrypted = await ecbEncryptBlock(k1, tempBlock, crypto);
208
+
209
+ // C = CC XOR T
210
+ xorBlocks(
211
+ output.subarray(blockOffset, blockOffset + BLOCK_SIZE),
212
+ encrypted,
213
+ T
214
+ );
215
+
216
+ // T = T * alpha in GF(2^128)
217
+ gf128Mul(T);
218
+ }
219
+ }
220
+
221
+ return output.buffer;
222
+ }
223
+
224
+ /**
225
+ * Decrypt data using AES-128-XTS with Nintendo's big-endian tweak.
226
+ *
227
+ * @param key - 32-byte key (first 16 bytes = data key K1, last 16 bytes = tweak key K2)
228
+ * @param data - Data to decrypt (must be a multiple of `sectorSize`)
229
+ * @param sectorSize - Size of each sector in bytes (typically 0x200 for NCA headers)
230
+ * @param startSector - Starting sector number (default: 0)
231
+ * @param crypto - Optional Crypto implementation (defaults to globalThis.crypto)
232
+ * @returns Decrypted data
233
+ */
234
+ export async function decrypt(
235
+ key: ArrayBuffer | Uint8Array,
236
+ data: ArrayBuffer | Uint8Array,
237
+ sectorSize: number,
238
+ startSector = 0,
239
+ crypto: Crypto = globalThis.crypto
240
+ ): Promise<ArrayBuffer> {
241
+ const keyBytes = new Uint8Array(key);
242
+ if (keyBytes.length !== 32) {
243
+ throw new Error(`AES-XTS key must be 32 bytes, got ${keyBytes.length}`);
244
+ }
245
+
246
+ const dataBytes = new Uint8Array(data);
247
+ if (dataBytes.length % sectorSize !== 0) {
248
+ throw new Error('Data length must be a multiple of sector size');
249
+ }
250
+ if (sectorSize % BLOCK_SIZE !== 0) {
251
+ throw new Error('Sector size must be a multiple of 16');
252
+ }
253
+
254
+ const k1 = await importKey(keyBytes.subarray(0, 16), crypto);
255
+ const k2 = await importKey(keyBytes.subarray(16, 32), crypto);
256
+
257
+ const output = new Uint8Array(dataBytes.length);
258
+ const blocksPerSector = sectorSize / BLOCK_SIZE;
259
+ const tempBlock = new Uint8Array(BLOCK_SIZE);
260
+
261
+ for (
262
+ let sectorOffset = 0;
263
+ sectorOffset < dataBytes.length;
264
+ sectorOffset += sectorSize
265
+ ) {
266
+ const sector = startSector + sectorOffset / sectorSize;
267
+
268
+ // Compute the initial tweak: T = AES-ECB-Encrypt(K2, tweak_value)
269
+ const tweakValue = getNintendoTweak(sector);
270
+ const T = await ecbEncryptBlock(k2, tweakValue, crypto);
271
+
272
+ for (let j = 0; j < blocksPerSector; j++) {
273
+ const blockOffset = sectorOffset + j * BLOCK_SIZE;
274
+ const cipherBlock = dataBytes.subarray(
275
+ blockOffset,
276
+ blockOffset + BLOCK_SIZE
277
+ );
278
+
279
+ // CC = C XOR T
280
+ xorBlocks(tempBlock, cipherBlock, T);
281
+
282
+ // PP = AES-ECB-Decrypt(K1, CC)
283
+ const decrypted = await ecbDecryptBlock(k1, tempBlock, crypto);
284
+
285
+ // P = PP XOR T
286
+ xorBlocks(
287
+ output.subarray(blockOffset, blockOffset + BLOCK_SIZE),
288
+ decrypted,
289
+ T
290
+ );
291
+
292
+ // T = T * alpha in GF(2^128)
293
+ gf128Mul(T);
294
+ }
295
+ }
296
+
297
+ return output.buffer;
298
+ }
@@ -0,0 +1,114 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { encrypt, decrypt } from '../src/index.js';
3
+
4
+ // Test vectors generated using Node.js crypto with identical key/tweak settings
5
+ const TEST_KEY = hexToBytes(
6
+ '00112233445566778899aabbccddeeffaabbccddeeff00112233445566778899'
7
+ );
8
+
9
+ const SECTOR_0_EXPECTED =
10
+ '7575d42fde6b2f7190ff26861970b889b0f7d93951047e4913017c4a6dd4a1cc';
11
+
12
+ const SECTOR_1_EXPECTED =
13
+ 'd573fc38797f8affbe2bd3b104b0ef085667c568fed42c7773f8e936e780d1f5';
14
+
15
+ function hexToBytes(hex: string): Uint8Array {
16
+ const bytes = new Uint8Array(hex.length / 2);
17
+ for (let i = 0; i < bytes.length; i++) {
18
+ bytes[i] = parseInt(hex.substring(i * 2, i * 2 + 2), 16);
19
+ }
20
+ return bytes;
21
+ }
22
+
23
+ function bytesToHex(bytes: Uint8Array): string {
24
+ return Array.from(bytes)
25
+ .map((b) => b.toString(16).padStart(2, '0'))
26
+ .join('');
27
+ }
28
+
29
+ function makeTestData(size: number): Uint8Array {
30
+ const data = new Uint8Array(size);
31
+ for (let i = 0; i < size; i++) data[i] = i & 0xff;
32
+ return data;
33
+ }
34
+
35
+ describe('AES-128-XTS with Nintendo big-endian tweak', () => {
36
+ describe('encrypt', () => {
37
+ it('should encrypt a single sector (sector 0)', async () => {
38
+ const plain = makeTestData(512);
39
+ const ct = new Uint8Array(await encrypt(TEST_KEY, plain, 512, 0));
40
+ expect(bytesToHex(ct.subarray(0, 32))).toBe(SECTOR_0_EXPECTED);
41
+ });
42
+
43
+ it('should encrypt with correct sector 1 tweak', async () => {
44
+ const plain = makeTestData(512);
45
+ const ct = new Uint8Array(await encrypt(TEST_KEY, plain, 512, 1));
46
+ expect(bytesToHex(ct.subarray(0, 32))).toBe(SECTOR_1_EXPECTED);
47
+ });
48
+
49
+ it('should encrypt multiple sectors', async () => {
50
+ const plain = makeTestData(1024);
51
+ const ct = new Uint8Array(await encrypt(TEST_KEY, plain, 512, 0));
52
+ // First 512 bytes = sector 0 ciphertext
53
+ expect(bytesToHex(ct.subarray(0, 32))).toBe(SECTOR_0_EXPECTED);
54
+ // Second 512 bytes = sector 1 ciphertext
55
+ expect(bytesToHex(ct.subarray(512, 544))).toBe(SECTOR_1_EXPECTED);
56
+ });
57
+ });
58
+
59
+ describe('decrypt', () => {
60
+ it('should round-trip encrypt/decrypt', async () => {
61
+ const plain = makeTestData(512);
62
+ const ct = await encrypt(TEST_KEY, plain, 512, 0);
63
+ const decrypted = new Uint8Array(
64
+ await decrypt(TEST_KEY, ct, 512, 0)
65
+ );
66
+ expect(bytesToHex(decrypted)).toBe(bytesToHex(plain));
67
+ });
68
+
69
+ it('should round-trip multiple sectors', async () => {
70
+ const plain = makeTestData(1024);
71
+ const ct = await encrypt(TEST_KEY, plain, 512, 0);
72
+ const decrypted = new Uint8Array(
73
+ await decrypt(TEST_KEY, ct, 512, 0)
74
+ );
75
+ expect(bytesToHex(decrypted)).toBe(bytesToHex(plain));
76
+ });
77
+
78
+ it('should round-trip with non-zero start sector', async () => {
79
+ const plain = makeTestData(512);
80
+ const ct = await encrypt(TEST_KEY, plain, 512, 5);
81
+ const decrypted = new Uint8Array(
82
+ await decrypt(TEST_KEY, ct, 512, 5)
83
+ );
84
+ expect(bytesToHex(decrypted)).toBe(bytesToHex(plain));
85
+ });
86
+ });
87
+
88
+ describe('NCA header size (0xC00 = 3072 bytes, sector size 0x200)', () => {
89
+ it('should encrypt/decrypt NCA-header-sized data', async () => {
90
+ const data = makeTestData(0xc00);
91
+ const ct = await encrypt(TEST_KEY, data, 0x200, 0);
92
+ const decrypted = new Uint8Array(
93
+ await decrypt(TEST_KEY, ct, 0x200, 0)
94
+ );
95
+ expect(bytesToHex(decrypted)).toBe(bytesToHex(data));
96
+ });
97
+ });
98
+
99
+ describe('validation', () => {
100
+ it('should reject non-32-byte keys', async () => {
101
+ const plain = makeTestData(512);
102
+ await expect(
103
+ encrypt(new Uint8Array(16), plain, 512)
104
+ ).rejects.toThrow('32 bytes');
105
+ });
106
+
107
+ it('should reject data not aligned to sector size', async () => {
108
+ const plain = makeTestData(500);
109
+ await expect(encrypt(TEST_KEY, plain, 512)).rejects.toThrow(
110
+ 'multiple of sector size'
111
+ );
112
+ });
113
+ });
114
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es2020",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "outDir": "./dist",
7
+ "declaration": true,
8
+ "sourceMap": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "strict": true,
11
+ "skipLibCheck": true
12
+ },
13
+ "include": ["src/**/*.ts"]
14
+ }