miijs 2.4.2 → 2.5.1
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.
- package/README.md +51 -44
- package/amiiboHandler.js +169 -25
- package/asmCrypto.js +16 -8
- package/dist/miijs.browser.esm.js +7628 -0
- package/dist/miijs.browser.esm.js.map +7 -0
- package/dist/miijs.browser.js +7628 -0
- package/dist/miijs.browser.js.map +7 -0
- package/index.js +459 -144
- package/package.json +36 -2
- package/shims/amiibo-stub.js +11 -0
- package/shims/crypto-browser.js +4 -0
- package/shims/empty.js +2 -0
- package/shims/ffl-wrapper-stub.js +13 -0
- package/shims/path-browser.js +26 -0
- package/.github/workflows/npm-publish-github-packages.yml +0 -36
- package/crown.jpg +0 -0
- package/ideal.jsonc +0 -91
- package/mii.js +0 -3346
- package/miiFemaleBody.glb +0 -0
- package/miiMaleBody.glb +0 -0
- package/miijs.png +0 -0
package/README.md
CHANGED
|
@@ -47,9 +47,10 @@ MiiJS is a complete and comprehensive Mii library for reading, converting, modif
|
|
|
47
47
|
- **`miiHeightToMeasurements(miiHeight)`** - Converts Mii height value (0-127) to real-world feet and inches. Returns `{feet, inches, totalInches, centimeters}`.
|
|
48
48
|
- **`inchesToMiiHeight(totalInches)`** - Converts real-world height in inches to Mii height value (0-127).
|
|
49
49
|
- **`centimetersToMiiHeight(totalCentimeters)`** - Converts real-world height in centimeters to Mii height value (0-127).
|
|
50
|
-
- **`
|
|
50
|
+
- **`miiWeightToRealWeight(miiWeight)`** - Converts Mii weight value (0-127) to real-world weight values. Returns `{pounds, kilograms}`.
|
|
51
51
|
- **`imperialHeightWeightToMiiWeight(heightInches, weightLbs)`** - Converts real-world imperial measurements to Mii weight values.
|
|
52
52
|
- **`metricHeightWeightToMiiWeight(heightCentimeters, weightKilograms)`** - Converts real-world metric measurements to Mii weight values.
|
|
53
|
+
- **`miiIdToTimestamp(miiId, consoleMiiIdIsFrom)`** - Converts the Mii ID into a JS Date/Timestamp.
|
|
53
54
|
|
|
54
55
|
### Other Functions
|
|
55
56
|
- **`makeChild(miiJson1, miiJson2, options?)`** - Returns an array of 6 different Mii JSONs, which represent a child generated from the two parent Miis passed to the function at different stages of life. This is somewhat experimental, but should be accurate to my current knowledge. You can pass any or none of { name: "The Name", creatorName: "The Name", favoriteColor: 0-11, gender: 0-1/\*0:Male, 1:Female\*/ }
|
|
@@ -62,12 +63,11 @@ MiiJS is a complete and comprehensive Mii library for reading, converting, modif
|
|
|
62
63
|
```javascript
|
|
63
64
|
const miijs = require('miijs');
|
|
64
65
|
|
|
65
|
-
// Read from file path
|
|
66
|
+
// Read from file path for image, or buffer of the data - encrypted or decrypted
|
|
66
67
|
const miiJson = await miijs.read3DSQR('./example3DSQR.jpg');
|
|
67
68
|
console.log('Mii Name:', miiJson.meta.name);
|
|
68
|
-
console.log('Favorite Color:', miiJson.general.favoriteColor);
|
|
69
69
|
|
|
70
|
-
//
|
|
70
|
+
// Get the decrypted binary data
|
|
71
71
|
const decryptedBin = await miijs.read3DSQR('./example3DSQR.jpg', true);
|
|
72
72
|
console.log('Decrypted binary length:', decryptedBin.length);
|
|
73
73
|
```
|
|
@@ -79,7 +79,6 @@ const miijs = require('miijs');
|
|
|
79
79
|
// Read from file path
|
|
80
80
|
const miiJson = await miijs.readWiiBin('./exampleWii.bin');
|
|
81
81
|
console.log('Mii Name:', miiJson.meta.name);
|
|
82
|
-
console.log('Gender:', miiJson.general.gender === 0 ? 'Male' : 'Female');
|
|
83
82
|
|
|
84
83
|
// Or pass binary data directly
|
|
85
84
|
const fs = require('fs');
|
|
@@ -94,13 +93,10 @@ const miijs = require('miijs');
|
|
|
94
93
|
// First, read or create a Mii JSON
|
|
95
94
|
const miiJson = await miijs.read3DSQR('./example3DSQR.jpg');
|
|
96
95
|
|
|
97
|
-
// Write QR code
|
|
96
|
+
// Write QR code. If FFLResHigh.dat is in the same directory, will be used automatically. You can pass a buffer containing FFLResHigh.dat as a fourth parameter. Will use Studio rendering instead of local without FFLResHigh.dat.
|
|
98
97
|
await miijs.write3DSQR(miiJson, './output_qr.jpg');
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const fs = require('fs');
|
|
102
|
-
const fflRes = fs.readFileSync('./FFLResHigh.dat');
|
|
103
|
-
await miijs.write3DSQR(miiJson, './output_qr_local.jpg', fflRes);
|
|
98
|
+
//The third parameter asks MiiJS to return an encrypted 3DS data instead
|
|
99
|
+
const encryptedData=await miijs.write3DSQR(miiJson,'',true);
|
|
104
100
|
```
|
|
105
101
|
|
|
106
102
|
## Writing a Wii Mii Binary
|
|
@@ -108,11 +104,8 @@ await miijs.write3DSQR(miiJson, './output_qr_local.jpg', fflRes);
|
|
|
108
104
|
const miijs = require('miijs');
|
|
109
105
|
const fs = require('fs');
|
|
110
106
|
|
|
111
|
-
// Read a Mii
|
|
112
|
-
const miiJson = await miijs.
|
|
113
|
-
|
|
114
|
-
// Convert to Wii format first if needed
|
|
115
|
-
const wiiMii = miijs.convertMii(miiJson, 'wii');
|
|
107
|
+
// Read a Mii
|
|
108
|
+
const miiJson = await miijs.readWiiBin('./exampleWii.bin');
|
|
116
109
|
|
|
117
110
|
// Write to file
|
|
118
111
|
await miijs.writeWiiBin(wiiMii, './output_wii.bin');
|
|
@@ -127,16 +120,16 @@ fs.writeFileSync('./manual_write.bin', buffer);
|
|
|
127
120
|
const miijs = require('miijs');
|
|
128
121
|
|
|
129
122
|
// Read a 3DS Mii
|
|
130
|
-
const
|
|
123
|
+
const mii3DS = await miijs.read3DSQR('./example3DSQR.jpg');
|
|
131
124
|
|
|
132
125
|
// Convert to Wii format
|
|
133
|
-
const
|
|
126
|
+
const miiWii = miijs.convertMii(mii3DS, 'wii');
|
|
134
127
|
|
|
135
128
|
// Convert back to 3DS
|
|
136
|
-
const backTo3DS = miijs.convertMii(
|
|
129
|
+
const backTo3DS = miijs.convertMii(miiWii, '3ds');
|
|
137
130
|
|
|
138
131
|
// Auto-detect and convert to opposite
|
|
139
|
-
const autoConverted = miijs.convertMii(
|
|
132
|
+
const autoConverted = miijs.convertMii(mii3DS);
|
|
140
133
|
```
|
|
141
134
|
|
|
142
135
|
## Converting to/from Studio Format
|
|
@@ -151,7 +144,6 @@ console.log('Studio URL:', `https://studio.mii.nintendo.com/miis/image.png?data=
|
|
|
151
144
|
// Convert Studio format back to JSON
|
|
152
145
|
const studioData = '000d142a303f434b717a7b84939ba6b2bbbec5cbc9d0e2ea...';
|
|
153
146
|
const miiFromStudio = miijs.convertStudioToMii(studioData);
|
|
154
|
-
console.log('Converted Mii:', miiFromStudio.meta.name);
|
|
155
147
|
```
|
|
156
148
|
|
|
157
149
|
## Rendering Miis
|
|
@@ -162,16 +154,15 @@ const fs = require('fs');
|
|
|
162
154
|
// Read a Mii
|
|
163
155
|
const miiJson = await miijs.read3DSQR('./example3DSQR.jpg');
|
|
164
156
|
|
|
165
|
-
// Render using Studio API (simple, no setup needed)
|
|
157
|
+
// Render using Studio API (simple, no setup needed, requires internet access)
|
|
166
158
|
const studioPng = await miijs.renderMiiWithStudio(miiJson);
|
|
167
159
|
fs.writeFileSync('./mii_studio_render.png', studioPng);
|
|
168
160
|
|
|
169
161
|
// Render locally with full body (requires FFLResHigh.dat)
|
|
162
|
+
// FFLResHigh.dat can be placed in the project directory to automatically use
|
|
170
163
|
const fflRes = fs.readFileSync('./FFLResHigh.dat');
|
|
171
164
|
const localPng = await miijs.renderMii(miiJson, fflRes);
|
|
172
165
|
fs.writeFileSync('./mii_local_render.png', localPng);
|
|
173
|
-
|
|
174
|
-
// Shirt color comes from miiJson.general.favoriteColor
|
|
175
166
|
```
|
|
176
167
|
|
|
177
168
|
## Working with Amiibos
|
|
@@ -183,13 +174,11 @@ const fs = require('fs');
|
|
|
183
174
|
const amiiboDump = fs.readFileSync('./exampleAmiiboDump.bin');
|
|
184
175
|
|
|
185
176
|
// Extract the Mii from the Amiibo (returns 92 bytes decrypted)
|
|
186
|
-
|
|
187
|
-
|
|
177
|
+
let miiData = miijs.extractMiiFromAmiibo(amiiboDump);
|
|
188
178
|
// Convert the raw Mii data to readable JSON
|
|
189
|
-
|
|
190
|
-
const miiJson = miijs.decode3DSMii(miiData); // Note: decode3DSMii not exported, use read3DSQR workflow
|
|
179
|
+
const miiJson = miijs.read3DSQR(miiData);
|
|
191
180
|
|
|
192
|
-
//
|
|
181
|
+
// Read from QR, get decrypted data, insert into Amiibo
|
|
193
182
|
const qrMiiJson = await miijs.read3DSQR('./example3DSQR.jpg');
|
|
194
183
|
const decryptedMiiData = await miijs.read3DSQR('./example3DSQR.jpg', true);
|
|
195
184
|
|
|
@@ -224,23 +213,28 @@ Object.entries(fullInstructions).forEach(([field, instruction]) => {
|
|
|
224
213
|
```javascript
|
|
225
214
|
const miijs = require('miijs');
|
|
226
215
|
|
|
227
|
-
// Convert Mii height (0-127) to feet/inches
|
|
228
|
-
const heightInfo = miijs.
|
|
229
|
-
console.log(`Height: ${heightInfo.feet}'${heightInfo.inches}" (${heightInfo.
|
|
216
|
+
// Convert Mii height (0-127) to feet/inches and centimeters
|
|
217
|
+
const heightInfo = miijs.miiHeightToMeasurements(64); // midpoint value
|
|
218
|
+
console.log(`Height: ${heightInfo.feet}'${heightInfo.inches}" (${heightInfo.centimeters} cm)`);
|
|
230
219
|
|
|
231
220
|
// Convert real height to Mii value
|
|
232
|
-
const miiHeightValue = miijs.inchesToMiiHeight(72);
|
|
221
|
+
const miiHeightValue = miijs.inchesToMiiHeight(72);
|
|
233
222
|
console.log('Mii height value for 6\'0":', miiHeightValue);
|
|
234
223
|
|
|
235
|
-
//
|
|
236
|
-
const
|
|
237
|
-
const
|
|
238
|
-
const
|
|
224
|
+
// Convert Mii weight to real weight (requires height)
|
|
225
|
+
const miiHeight = 64;
|
|
226
|
+
const miiWeight = 64;
|
|
227
|
+
const weightInfo = miijs.miiWeightToRealWeight(miiHeight, miiWeight);
|
|
228
|
+
console.log(`Weight: ${weightInfo.pounds.toFixed(1)} lbs (${weightInfo.kilograms} kg)`);
|
|
229
|
+
|
|
230
|
+
// Convert real weight to Mii weight value
|
|
231
|
+
const heightInches=70;
|
|
232
|
+
const weightLbs = 150;
|
|
233
|
+
const miiWeightValue = miijs.imperialHeightWeightToMiiWeight(heightInches, weightLbs);
|
|
239
234
|
console.log('Mii weight value:', miiWeightValue);
|
|
240
235
|
|
|
241
|
-
//
|
|
242
|
-
const
|
|
243
|
-
console.log(`Weight: ${weightInfo.pounds.toFixed(1)} lbs, BMI: ${weightInfo.bmi.toFixed(1)}`);
|
|
236
|
+
// Or use metric
|
|
237
|
+
const miiWeightMetric = miijs.metricHeightWeightToMiiWeight(175, 72.5);
|
|
244
238
|
```
|
|
245
239
|
|
|
246
240
|
## Creating and Modifying a Mii
|
|
@@ -251,10 +245,10 @@ const miijs = require('miijs');
|
|
|
251
245
|
const miiJson = await miijs.read3DSQR('./example3DSQR.jpg');
|
|
252
246
|
|
|
253
247
|
// Modify properties
|
|
254
|
-
miiJson.meta.name = '
|
|
255
|
-
miiJson.general.favoriteColor = 5;
|
|
256
|
-
miiJson.hair.color = 0;
|
|
257
|
-
miiJson.eyes.color = 2;
|
|
248
|
+
miiJson.meta.name = 'CustomName';
|
|
249
|
+
miiJson.general.favoriteColor = 5;
|
|
250
|
+
miiJson.hair.color = 0;
|
|
251
|
+
miiJson.eyes.color = 2;
|
|
258
252
|
|
|
259
253
|
// Make it a Special Mii (3DS only)
|
|
260
254
|
miiJson.meta.type = 'Special';
|
|
@@ -287,6 +281,19 @@ for(var i=0;i<child.length;i++){
|
|
|
287
281
|
}
|
|
288
282
|
```
|
|
289
283
|
|
|
284
|
+
## Getting the Time the Mii Was Created From the Mii ID
|
|
285
|
+
```javascript
|
|
286
|
+
const miijs=require('miijs');
|
|
287
|
+
|
|
288
|
+
//Read miis into JSON
|
|
289
|
+
const mii3DS=await miijs.read3DSQR('./example_mii.jpg');
|
|
290
|
+
const miiWii=await miijs.readWiiBin('./example_mii.bin');
|
|
291
|
+
|
|
292
|
+
//Same process for both, returns a JS Date/Timestamp
|
|
293
|
+
console.log(miijs.miiIdToTimestamp(mii3DS.meta.miiId,mii3DS.console));
|
|
294
|
+
console.log(miijs.miiIdToTimestamp(miiWii.meta.miiId,miiWii.console));
|
|
295
|
+
```
|
|
296
|
+
|
|
290
297
|
<hr>
|
|
291
298
|
|
|
292
299
|
## Special Miis
|
package/amiiboHandler.js
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
const
|
|
1
|
+
const { Buffer } = require('buffer');
|
|
2
|
+
|
|
3
|
+
const isBrowser = typeof window !== 'undefined' && typeof window.crypto !== 'undefined';
|
|
4
|
+
const isNode = typeof process !== 'undefined' && process.versions != null && process.versions.node != null;
|
|
5
|
+
|
|
6
|
+
const nodeCrypto = isNode ? require('crypto') : null;
|
|
7
|
+
const subtleCrypto = (typeof globalThis !== 'undefined' && globalThis.crypto && globalThis.crypto.subtle)
|
|
8
|
+
? globalThis.crypto.subtle
|
|
9
|
+
: (nodeCrypto && nodeCrypto.webcrypto ? nodeCrypto.webcrypto.subtle : null);
|
|
2
10
|
|
|
3
11
|
/*This constant is provided SOLELY because I cannot find a guide online to retrieve this file from a console or Amiibo on your own that doesn't just tell you to download it from somewhere anyway.
|
|
4
12
|
If someone can find, or make, a guide for this, I will wipe all commits of this key from the repo and instead point to how to get this key for yourself.*/
|
|
@@ -89,7 +97,7 @@ function drbgGenerateBytes(hmacKey, seed, outputSize) {
|
|
|
89
97
|
iterBuffer[0] = (iteration >> 8) & 0xFF;
|
|
90
98
|
iterBuffer[1] = iteration & 0xFF;
|
|
91
99
|
seed.copy(iterBuffer, 2);
|
|
92
|
-
const hmac =
|
|
100
|
+
const hmac = nodeCrypto.createHmac('sha256', hmacKey);
|
|
93
101
|
hmac.update(iterBuffer);
|
|
94
102
|
const output = hmac.digest();
|
|
95
103
|
const toCopy = Math.min(32, outputSize - offset);
|
|
@@ -100,6 +108,24 @@ function drbgGenerateBytes(hmacKey, seed, outputSize) {
|
|
|
100
108
|
return result;
|
|
101
109
|
}
|
|
102
110
|
|
|
111
|
+
async function drbgGenerateBytesAsync(hmacKey, seed, outputSize) {
|
|
112
|
+
const result = Buffer.alloc(outputSize);
|
|
113
|
+
let offset = 0;
|
|
114
|
+
let iteration = 0;
|
|
115
|
+
while (offset < outputSize) {
|
|
116
|
+
const iterBuffer = Buffer.alloc(2 + seed.length);
|
|
117
|
+
iterBuffer[0] = (iteration >> 8) & 0xFF;
|
|
118
|
+
iterBuffer[1] = iteration & 0xFF;
|
|
119
|
+
seed.copy(iterBuffer, 2);
|
|
120
|
+
const output = await hmacSha256Async(hmacKey, iterBuffer);
|
|
121
|
+
const toCopy = Math.min(32, outputSize - offset);
|
|
122
|
+
output.copy(result, offset, 0, toCopy);
|
|
123
|
+
offset += toCopy;
|
|
124
|
+
iteration++;
|
|
125
|
+
}
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
|
|
103
129
|
function deriveKeys(typeString, magicBytes, magicBytesSize, xorPad, hmacKey, baseSeed) {
|
|
104
130
|
const preparedSeed = prepareSeed(typeString, magicBytes, magicBytesSize, xorPad, baseSeed);
|
|
105
131
|
const derived = drbgGenerateBytes(hmacKey, preparedSeed, 48);
|
|
@@ -110,6 +136,66 @@ function deriveKeys(typeString, magicBytes, magicBytesSize, xorPad, hmacKey, bas
|
|
|
110
136
|
};
|
|
111
137
|
}
|
|
112
138
|
|
|
139
|
+
async function deriveKeysAsync(typeString, magicBytes, magicBytesSize, xorPad, hmacKey, baseSeed) {
|
|
140
|
+
const preparedSeed = prepareSeed(typeString, magicBytes, magicBytesSize, xorPad, baseSeed);
|
|
141
|
+
const derived = await drbgGenerateBytesAsync(hmacKey, preparedSeed, 48);
|
|
142
|
+
return {
|
|
143
|
+
aesKey: derived.slice(0, 16),
|
|
144
|
+
aesIV: derived.slice(16, 32),
|
|
145
|
+
hmacKey: derived.slice(32, 48)
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function ensureBuffer(input, name) {
|
|
150
|
+
if (Buffer.isBuffer(input)) {
|
|
151
|
+
return input;
|
|
152
|
+
}
|
|
153
|
+
if (input instanceof Uint8Array) {
|
|
154
|
+
return Buffer.from(input);
|
|
155
|
+
}
|
|
156
|
+
if (input instanceof ArrayBuffer) {
|
|
157
|
+
return Buffer.from(new Uint8Array(input));
|
|
158
|
+
}
|
|
159
|
+
throw new Error(`${name} must be a Buffer or Uint8Array`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function hmacSha256Async(key, data) {
|
|
163
|
+
if (!subtleCrypto) {
|
|
164
|
+
throw new Error('Web Crypto API is not available');
|
|
165
|
+
}
|
|
166
|
+
const cryptoKey = await subtleCrypto.importKey(
|
|
167
|
+
'raw',
|
|
168
|
+
ensureBuffer(key, 'HMAC key'),
|
|
169
|
+
{ name: 'HMAC', hash: 'SHA-256' },
|
|
170
|
+
false,
|
|
171
|
+
['sign']
|
|
172
|
+
);
|
|
173
|
+
const signature = await subtleCrypto.sign('HMAC', cryptoKey, ensureBuffer(data, 'HMAC data'));
|
|
174
|
+
return Buffer.from(new Uint8Array(signature));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function aesCtrCryptAsync(key, iv, data, encrypt) {
|
|
178
|
+
if (!subtleCrypto) {
|
|
179
|
+
throw new Error('Web Crypto API is not available');
|
|
180
|
+
}
|
|
181
|
+
const cryptoKey = await subtleCrypto.importKey(
|
|
182
|
+
'raw',
|
|
183
|
+
ensureBuffer(key, 'AES key'),
|
|
184
|
+
{ name: 'AES-CTR' },
|
|
185
|
+
false,
|
|
186
|
+
encrypt ? ['encrypt'] : ['decrypt']
|
|
187
|
+
);
|
|
188
|
+
const algorithm = {
|
|
189
|
+
name: 'AES-CTR',
|
|
190
|
+
counter: ensureBuffer(iv, 'AES IV'),
|
|
191
|
+
length: 128,
|
|
192
|
+
};
|
|
193
|
+
const result = encrypt
|
|
194
|
+
? await subtleCrypto.encrypt(algorithm, cryptoKey, ensureBuffer(data, 'AES data'))
|
|
195
|
+
: await subtleCrypto.decrypt(algorithm, cryptoKey, ensureBuffer(data, 'AES data'));
|
|
196
|
+
return Buffer.from(new Uint8Array(result));
|
|
197
|
+
}
|
|
198
|
+
|
|
113
199
|
function tagToInternal(tag) {
|
|
114
200
|
const internal = Buffer.alloc(NFC3D_AMIIBO_SIZE);
|
|
115
201
|
tag.slice(0x008, 0x010).copy(internal, 0x000);
|
|
@@ -140,38 +226,56 @@ function decryptAmiibo(tag) {
|
|
|
140
226
|
const dataKeys = deriveKeys(DATA_TYPE_STRING, DATA_MAGIC_BYTES, DATA_MAGIC_BYTES_SIZE, DATA_XOR_PAD, DATA_HMAC_KEY, seed);
|
|
141
227
|
const tagKeys = deriveKeys(TAG_TYPE_STRING, TAG_MAGIC_BYTES, TAG_MAGIC_BYTES_SIZE, TAG_XOR_PAD, TAG_HMAC_KEY, seed);
|
|
142
228
|
const plain = Buffer.alloc(NFC3D_AMIIBO_SIZE);
|
|
143
|
-
const cipher =
|
|
229
|
+
const cipher = nodeCrypto.createDecipheriv('aes-128-ctr', dataKeys.aesKey, dataKeys.aesIV);
|
|
144
230
|
cipher.setAutoPadding(false);
|
|
145
231
|
const decrypted = cipher.update(internal.slice(0x02C, 0x1B4));
|
|
146
232
|
decrypted.copy(plain, 0x02C);
|
|
147
233
|
internal.slice(0x000, 0x008).copy(plain, 0x000);
|
|
148
234
|
internal.slice(0x028, 0x02C).copy(plain, 0x028);
|
|
149
235
|
internal.slice(0x1D4, 0x208).copy(plain, 0x1D4);
|
|
150
|
-
const tagHmac =
|
|
236
|
+
const tagHmac = nodeCrypto.createHmac('sha256', tagKeys.hmacKey);
|
|
151
237
|
tagHmac.update(plain.slice(0x1D4, 0x208));
|
|
152
238
|
const computedTagHmac = tagHmac.digest();
|
|
153
239
|
computedTagHmac.copy(plain, 0x1B4);
|
|
154
|
-
const dataHmac =
|
|
240
|
+
const dataHmac = nodeCrypto.createHmac('sha256', dataKeys.hmacKey);
|
|
155
241
|
dataHmac.update(plain.slice(0x029, 0x208));
|
|
156
242
|
const computedDataHmac = dataHmac.digest();
|
|
157
243
|
computedDataHmac.copy(plain, 0x008);
|
|
158
244
|
return plain;
|
|
159
245
|
}
|
|
160
246
|
|
|
247
|
+
async function decryptAmiiboAsync(tag) {
|
|
248
|
+
const internal = tagToInternal(tag);
|
|
249
|
+
const seed = calcSeed(internal);
|
|
250
|
+
const dataKeys = await deriveKeysAsync(DATA_TYPE_STRING, DATA_MAGIC_BYTES, DATA_MAGIC_BYTES_SIZE, DATA_XOR_PAD, DATA_HMAC_KEY, seed);
|
|
251
|
+
const tagKeys = await deriveKeysAsync(TAG_TYPE_STRING, TAG_MAGIC_BYTES, TAG_MAGIC_BYTES_SIZE, TAG_XOR_PAD, TAG_HMAC_KEY, seed);
|
|
252
|
+
const plain = Buffer.alloc(NFC3D_AMIIBO_SIZE);
|
|
253
|
+
const decrypted = await aesCtrCryptAsync(dataKeys.aesKey, dataKeys.aesIV, internal.slice(0x02C, 0x1B4), false);
|
|
254
|
+
decrypted.copy(plain, 0x02C);
|
|
255
|
+
internal.slice(0x000, 0x008).copy(plain, 0x000);
|
|
256
|
+
internal.slice(0x028, 0x02C).copy(plain, 0x028);
|
|
257
|
+
internal.slice(0x1D4, 0x208).copy(plain, 0x1D4);
|
|
258
|
+
const tagHmac = await hmacSha256Async(tagKeys.hmacKey, plain.slice(0x1D4, 0x208));
|
|
259
|
+
tagHmac.copy(plain, 0x1B4);
|
|
260
|
+
const dataHmac = await hmacSha256Async(dataKeys.hmacKey, plain.slice(0x029, 0x208));
|
|
261
|
+
dataHmac.copy(plain, 0x008);
|
|
262
|
+
return plain;
|
|
263
|
+
}
|
|
264
|
+
|
|
161
265
|
function encryptAmiibo(plain) {
|
|
162
266
|
const seed = calcSeed(plain);
|
|
163
267
|
const dataKeys = deriveKeys(DATA_TYPE_STRING, DATA_MAGIC_BYTES, DATA_MAGIC_BYTES_SIZE, DATA_XOR_PAD, DATA_HMAC_KEY, seed);
|
|
164
268
|
const tagKeys = deriveKeys(TAG_TYPE_STRING, TAG_MAGIC_BYTES, TAG_MAGIC_BYTES_SIZE, TAG_XOR_PAD, TAG_HMAC_KEY, seed);
|
|
165
269
|
const cipher_internal = Buffer.alloc(NFC3D_AMIIBO_SIZE);
|
|
166
|
-
const tagHmac =
|
|
270
|
+
const tagHmac = nodeCrypto.createHmac('sha256', tagKeys.hmacKey);
|
|
167
271
|
tagHmac.update(plain.slice(0x1D4, 0x208));
|
|
168
272
|
tagHmac.digest().copy(cipher_internal, 0x1B4);
|
|
169
|
-
const dataHmac =
|
|
273
|
+
const dataHmac = nodeCrypto.createHmac('sha256', dataKeys.hmacKey);
|
|
170
274
|
dataHmac.update(plain.slice(0x029, 0x1B4));
|
|
171
275
|
dataHmac.update(cipher_internal.slice(0x1B4, 0x1D4));
|
|
172
276
|
dataHmac.update(plain.slice(0x1D4, 0x208));
|
|
173
277
|
dataHmac.digest().copy(cipher_internal, 0x008);
|
|
174
|
-
const aesCipher =
|
|
278
|
+
const aesCipher = nodeCrypto.createCipheriv('aes-128-ctr', dataKeys.aesKey, dataKeys.aesIV);
|
|
175
279
|
aesCipher.setAutoPadding(false);
|
|
176
280
|
const encrypted = aesCipher.update(plain.slice(0x02C, 0x1B4));
|
|
177
281
|
encrypted.copy(cipher_internal, 0x02C);
|
|
@@ -181,16 +285,38 @@ function encryptAmiibo(plain) {
|
|
|
181
285
|
return internalToTag(cipher_internal);
|
|
182
286
|
}
|
|
183
287
|
|
|
288
|
+
async function encryptAmiiboAsync(plain) {
|
|
289
|
+
const seed = calcSeed(plain);
|
|
290
|
+
const dataKeys = await deriveKeysAsync(DATA_TYPE_STRING, DATA_MAGIC_BYTES, DATA_MAGIC_BYTES_SIZE, DATA_XOR_PAD, DATA_HMAC_KEY, seed);
|
|
291
|
+
const tagKeys = await deriveKeysAsync(TAG_TYPE_STRING, TAG_MAGIC_BYTES, TAG_MAGIC_BYTES_SIZE, TAG_XOR_PAD, TAG_HMAC_KEY, seed);
|
|
292
|
+
const cipher_internal = Buffer.alloc(NFC3D_AMIIBO_SIZE);
|
|
293
|
+
const tagHmac = await hmacSha256Async(tagKeys.hmacKey, plain.slice(0x1D4, 0x208));
|
|
294
|
+
tagHmac.copy(cipher_internal, 0x1B4);
|
|
295
|
+
const dataHmac = await hmacSha256Async(dataKeys.hmacKey, Buffer.concat([
|
|
296
|
+
plain.slice(0x029, 0x1B4),
|
|
297
|
+
cipher_internal.slice(0x1B4, 0x1D4),
|
|
298
|
+
plain.slice(0x1D4, 0x208)
|
|
299
|
+
]));
|
|
300
|
+
dataHmac.copy(cipher_internal, 0x008);
|
|
301
|
+
const encrypted = await aesCtrCryptAsync(dataKeys.aesKey, dataKeys.aesIV, plain.slice(0x02C, 0x1B4), true);
|
|
302
|
+
encrypted.copy(cipher_internal, 0x02C);
|
|
303
|
+
plain.slice(0x000, 0x008).copy(cipher_internal, 0x000);
|
|
304
|
+
plain.slice(0x028, 0x02C).copy(cipher_internal, 0x028);
|
|
305
|
+
plain.slice(0x1D4, 0x208).copy(cipher_internal, 0x1D4);
|
|
306
|
+
return internalToTag(cipher_internal);
|
|
307
|
+
}
|
|
308
|
+
|
|
184
309
|
//Extract Mii data from an Amiibo dump
|
|
185
310
|
function extractMiiFromAmiibo(amiiboDump) {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
const size = amiiboDump.length;
|
|
311
|
+
const dump = ensureBuffer(amiiboDump, 'Amiibo dump');
|
|
312
|
+
const size = dump.length;
|
|
190
313
|
if (size !== NFC3D_AMIIBO_SIZE && size !== NTAG215_SIZE && size !== NTAG215_SIZE_ALT) {
|
|
191
314
|
throw new Error(`Invalid Amiibo dump size: ${size} (expected ${NFC3D_AMIIBO_SIZE}, ${NTAG215_SIZE_ALT}, or ${NTAG215_SIZE})`);
|
|
192
315
|
}
|
|
193
|
-
const tag =
|
|
316
|
+
const tag = dump.slice(0, NFC3D_AMIIBO_SIZE);
|
|
317
|
+
if (!isNode) {
|
|
318
|
+
return extractMiiFromAmiiboAsync(tag, dump.length);
|
|
319
|
+
}
|
|
194
320
|
const decrypted = decryptAmiibo(tag);
|
|
195
321
|
|
|
196
322
|
// Extract only the first 92 bytes (the actual Mii data, without checksum)
|
|
@@ -199,26 +325,31 @@ function extractMiiFromAmiibo(amiiboDump) {
|
|
|
199
325
|
return Buffer.from(miiData);
|
|
200
326
|
}
|
|
201
327
|
|
|
328
|
+
async function extractMiiFromAmiiboAsync(tag, dumpSize) {
|
|
329
|
+
const decrypted = await decryptAmiiboAsync(tag);
|
|
330
|
+
const miiData = decrypted.slice(MII_OFFSET_DECRYPTED, MII_OFFSET_DECRYPTED + 92);
|
|
331
|
+
return Buffer.from(miiData);
|
|
332
|
+
}
|
|
333
|
+
|
|
202
334
|
//Insert Mii data into an Amiibo dump
|
|
203
335
|
function insertMiiIntoAmiibo(amiiboDump, miiData) {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
if (!Buffer.isBuffer(miiData)) {
|
|
208
|
-
throw new Error('Mii data must be a Buffer');
|
|
209
|
-
}
|
|
210
|
-
const size = amiiboDump.length;
|
|
336
|
+
const dump = ensureBuffer(amiiboDump, 'Amiibo dump');
|
|
337
|
+
const miiBuf = ensureBuffer(miiData, 'Mii data');
|
|
338
|
+
const size = dump.length;
|
|
211
339
|
if (size !== NFC3D_AMIIBO_SIZE && size !== NTAG215_SIZE && size !== NTAG215_SIZE_ALT) {
|
|
212
340
|
throw new Error(`Invalid Amiibo dump size: ${size}`);
|
|
213
341
|
}
|
|
214
|
-
if (
|
|
215
|
-
throw new Error(`Mii data must be 92 or ${MII_SIZE} bytes, got ${
|
|
342
|
+
if (miiBuf.length !== 92 && miiBuf.length !== MII_SIZE) {
|
|
343
|
+
throw new Error(`Mii data must be 92 or ${MII_SIZE} bytes, got ${miiBuf.length}`);
|
|
344
|
+
}
|
|
345
|
+
const tag = dump.slice(0, NFC3D_AMIIBO_SIZE);
|
|
346
|
+
if (!isNode) {
|
|
347
|
+
return insertMiiIntoAmiiboAsync(tag, dump, miiBuf);
|
|
216
348
|
}
|
|
217
|
-
const tag = amiiboDump.slice(0, NFC3D_AMIIBO_SIZE);
|
|
218
349
|
const decrypted = decryptAmiibo(tag);
|
|
219
350
|
|
|
220
351
|
// Validate and fix Mii checksum, ensuring it's 96 bytes with correct checksum
|
|
221
|
-
const miiWithChecksum = validateAndFixMiiChecksum(
|
|
352
|
+
const miiWithChecksum = validateAndFixMiiChecksum(miiBuf);
|
|
222
353
|
|
|
223
354
|
// Insert Mii data (96 bytes)
|
|
224
355
|
miiWithChecksum.copy(decrypted, MII_OFFSET_DECRYPTED);
|
|
@@ -227,12 +358,25 @@ function insertMiiIntoAmiibo(amiiboDump, miiData) {
|
|
|
227
358
|
const result = Buffer.alloc(size);
|
|
228
359
|
encrypted.copy(result, 0);
|
|
229
360
|
if (size > NFC3D_AMIIBO_SIZE) {
|
|
230
|
-
|
|
361
|
+
dump.slice(NFC3D_AMIIBO_SIZE).copy(result, NFC3D_AMIIBO_SIZE);
|
|
231
362
|
}
|
|
232
363
|
|
|
233
364
|
return result;
|
|
234
365
|
}
|
|
235
366
|
|
|
367
|
+
async function insertMiiIntoAmiiboAsync(tag, dump, miiBuf) {
|
|
368
|
+
const decrypted = await decryptAmiiboAsync(tag);
|
|
369
|
+
const miiWithChecksum = validateAndFixMiiChecksum(miiBuf);
|
|
370
|
+
miiWithChecksum.copy(decrypted, MII_OFFSET_DECRYPTED);
|
|
371
|
+
const encrypted = await encryptAmiiboAsync(decrypted);
|
|
372
|
+
const result = Buffer.alloc(dump.length);
|
|
373
|
+
encrypted.copy(result, 0);
|
|
374
|
+
if (dump.length > NFC3D_AMIIBO_SIZE) {
|
|
375
|
+
dump.slice(NFC3D_AMIIBO_SIZE).copy(result, NFC3D_AMIIBO_SIZE);
|
|
376
|
+
}
|
|
377
|
+
return result;
|
|
378
|
+
}
|
|
379
|
+
|
|
236
380
|
module.exports = {
|
|
237
381
|
insertMiiIntoAmiibo,
|
|
238
382
|
extractMiiFromAmiibo
|
package/asmCrypto.js
CHANGED
|
@@ -3598,22 +3598,30 @@ function BigNumber_extGCD(a, b) {
|
|
|
3598
3598
|
}
|
|
3599
3599
|
|
|
3600
3600
|
function getRandomValues(buf) {
|
|
3601
|
-
if (typeof process !== 'undefined') {
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3601
|
+
if (typeof process !== 'undefined' && typeof require === 'function') {
|
|
3602
|
+
try {
|
|
3603
|
+
var nodeCrypto = eval('require')('crypto');
|
|
3604
|
+
var bytes = nodeCrypto.randomBytes(buf.length);
|
|
3605
|
+
buf.set(bytes);
|
|
3606
|
+
return;
|
|
3607
|
+
} catch (e) {
|
|
3608
|
+
// Fall through to browser methods
|
|
3609
|
+
}
|
|
3606
3610
|
}
|
|
3607
|
-
if (window.crypto && window.crypto.getRandomValues) {
|
|
3611
|
+
if (typeof window !== 'undefined' && window.crypto && window.crypto.getRandomValues) {
|
|
3608
3612
|
window.crypto.getRandomValues(buf);
|
|
3609
3613
|
return;
|
|
3610
3614
|
}
|
|
3611
|
-
if (self.crypto && self.crypto.getRandomValues) {
|
|
3615
|
+
if (typeof self !== 'undefined' && self.crypto && self.crypto.getRandomValues) {
|
|
3612
3616
|
self.crypto.getRandomValues(buf);
|
|
3613
3617
|
return;
|
|
3614
3618
|
}
|
|
3619
|
+
if (typeof globalThis !== 'undefined' && globalThis.crypto && globalThis.crypto.getRandomValues) {
|
|
3620
|
+
globalThis.crypto.getRandomValues(buf);
|
|
3621
|
+
return;
|
|
3622
|
+
}
|
|
3615
3623
|
// @ts-ignore
|
|
3616
|
-
if (window.msCrypto && window.msCrypto.getRandomValues) {
|
|
3624
|
+
if (typeof window !== 'undefined' && window.msCrypto && window.msCrypto.getRandomValues) {
|
|
3617
3625
|
// @ts-ignore
|
|
3618
3626
|
window.msCrypto.getRandomValues(buf);
|
|
3619
3627
|
return;
|