This introduces the ability to read and write game data and model information from an Amiibo dump file (BIN format). Note that this functionality requires the presence of a key_retail.bin file. For the option to appear and function in the UI, ensure that the key_retail.bin file is located in the <RyujinxData>/system folder.
388 lines
13 KiB
C#
388 lines
13 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Security.Cryptography;
|
|
|
|
namespace Ryujinx.HLE.HOS.Services.Nfc.AmiiboDecryption
|
|
{
|
|
public class AmiiboDump
|
|
{
|
|
private AmiiboMasterKey dataMasterKey;
|
|
private AmiiboMasterKey tagMasterKey;
|
|
|
|
private bool isLocked;
|
|
private byte[] data;
|
|
private byte[] hmacTagKey;
|
|
private byte[] hmacDataKey;
|
|
private byte[] aesKey;
|
|
private byte[] aesIv;
|
|
|
|
public AmiiboDump(byte[] dumpData, AmiiboMasterKey dataKey, AmiiboMasterKey tagKey, bool isLocked = true)
|
|
{
|
|
if (dumpData.Length < 540)
|
|
throw new ArgumentException("Incomplete dump. Amiibo data is at least 540 bytes.");
|
|
|
|
this.data = new byte[540];
|
|
Array.Copy(dumpData, this.data, dumpData.Length);
|
|
this.dataMasterKey = dataKey;
|
|
this.tagMasterKey = tagKey;
|
|
this.isLocked = isLocked;
|
|
|
|
if (!isLocked)
|
|
{
|
|
DeriveKeysAndCipher();
|
|
}
|
|
}
|
|
|
|
private byte[] DeriveKey(AmiiboMasterKey key, bool deriveAes, out byte[] derivedAesKey, out byte[] derivedAesIv)
|
|
{
|
|
List<byte> seed = new List<byte>();
|
|
|
|
// Start with the type string (14 bytes)
|
|
seed.AddRange(key.TypeString);
|
|
|
|
// Append data based on magic size
|
|
int append = 16 - key.MagicSize;
|
|
byte[] extract = new byte[16];
|
|
Array.Copy(this.data, 0x011, extract, 0, 2); // Extract two bytes from user data section
|
|
for (int i = 2; i < 16; i++)
|
|
{
|
|
extract[i] = 0x00;
|
|
}
|
|
seed.AddRange(extract.Take(append));
|
|
|
|
// Add the magic bytes
|
|
seed.AddRange(key.MagicBytes.Take(key.MagicSize));
|
|
|
|
// Extract the UID (UID is 8 bytes)
|
|
byte[] uid = new byte[8];
|
|
Array.Copy(this.data, 0x000, uid, 0, 8);
|
|
seed.AddRange(uid);
|
|
seed.AddRange(uid);
|
|
|
|
// Extract some tag data (pages 0x20 - 0x28)
|
|
byte[] user = new byte[32];
|
|
Array.Copy(this.data, 0x060, user, 0, 32);
|
|
|
|
// XOR it with the key padding (XorPad)
|
|
byte[] paddedUser = new byte[32];
|
|
for (int i = 0; i < user.Length; i++)
|
|
{
|
|
paddedUser[i] = (byte)(user[i] ^ key.XorPad[i]);
|
|
}
|
|
seed.AddRange(paddedUser);
|
|
|
|
byte[] seedBytes = seed.ToArray();
|
|
if (seedBytes.Length != 78)
|
|
{
|
|
throw new Exception("Size check for key derived seed failed");
|
|
}
|
|
|
|
byte[] hmacKey;
|
|
derivedAesKey = null;
|
|
derivedAesIv = null;
|
|
|
|
if (deriveAes)
|
|
{
|
|
// Derive AES Key and IV
|
|
var dataForAes = new byte[2 + seedBytes.Length];
|
|
dataForAes[0] = 0x00;
|
|
dataForAes[1] = 0x00; // Counter (0)
|
|
Array.Copy(seedBytes, 0, dataForAes, 2, seedBytes.Length);
|
|
|
|
byte[] derivedBytes;
|
|
using (var hmac = new HMACSHA256(key.HmacKey))
|
|
{
|
|
derivedBytes = hmac.ComputeHash(dataForAes);
|
|
}
|
|
|
|
derivedAesKey = derivedBytes.Take(16).ToArray();
|
|
derivedAesIv = derivedBytes.Skip(16).Take(16).ToArray();
|
|
|
|
// Derive HMAC Key
|
|
var dataForHmacKey = new byte[2 + seedBytes.Length];
|
|
dataForHmacKey[0] = 0x00;
|
|
dataForHmacKey[1] = 0x01; // Counter (1)
|
|
Array.Copy(seedBytes, 0, dataForHmacKey, 2, seedBytes.Length);
|
|
|
|
using (var hmac = new HMACSHA256(key.HmacKey))
|
|
{
|
|
derivedBytes = hmac.ComputeHash(dataForHmacKey);
|
|
}
|
|
|
|
hmacKey = derivedBytes.Take(16).ToArray();
|
|
}
|
|
else
|
|
{
|
|
// Derive HMAC Key only
|
|
var dataForHmacKey = new byte[2 + seedBytes.Length];
|
|
dataForHmacKey[0] = 0x00;
|
|
dataForHmacKey[1] = 0x01; // Counter (1)
|
|
Array.Copy(seedBytes, 0, dataForHmacKey, 2, seedBytes.Length);
|
|
|
|
byte[] derivedBytes;
|
|
using (var hmac = new HMACSHA256(key.HmacKey))
|
|
{
|
|
derivedBytes = hmac.ComputeHash(dataForHmacKey);
|
|
}
|
|
|
|
hmacKey = derivedBytes.Take(16).ToArray();
|
|
}
|
|
|
|
return hmacKey;
|
|
}
|
|
|
|
private void DeriveKeysAndCipher()
|
|
{
|
|
byte[] discard;
|
|
// Derive HMAC Tag Key
|
|
this.hmacTagKey = DeriveKey(this.tagMasterKey, false, out discard, out discard);
|
|
|
|
// Derive HMAC Data Key and AES Key/IV
|
|
this.hmacDataKey = DeriveKey(this.dataMasterKey, true, out aesKey, out aesIv);
|
|
}
|
|
|
|
private void DecryptData()
|
|
{
|
|
byte[] encryptedBlock = new byte[0x020 + 0x168];
|
|
Array.Copy(data, 0x014, encryptedBlock, 0, 0x020); // data[0x014:0x034]
|
|
Array.Copy(data, 0x0A0, encryptedBlock, 0x020, 0x168); // data[0x0A0:0x208]
|
|
|
|
byte[] decryptedBlock = AES_CTR_Transform(encryptedBlock, aesKey, aesIv);
|
|
|
|
// Copy decrypted data back
|
|
Array.Copy(decryptedBlock, 0, data, 0x014, 0x020);
|
|
Array.Copy(decryptedBlock, 0x020, data, 0x0A0, 0x168);
|
|
}
|
|
|
|
private void EncryptData()
|
|
{
|
|
byte[] plainBlock = new byte[0x020 + 0x168];
|
|
Array.Copy(data, 0x014, plainBlock, 0, 0x020); // data[0x014:0x034]
|
|
Array.Copy(data, 0x0A0, plainBlock, 0x020, 0x168); // data[0x0A0:0x208]
|
|
|
|
byte[] encryptedBlock = AES_CTR_Transform(plainBlock, aesKey, aesIv);
|
|
|
|
// Copy encrypted data back
|
|
Array.Copy(encryptedBlock, 0, data, 0x014, 0x020);
|
|
Array.Copy(encryptedBlock, 0x020, data, 0x0A0, 0x168);
|
|
}
|
|
|
|
private byte[] AES_CTR_Transform(byte[] data, byte[] key, byte[] iv)
|
|
{
|
|
byte[] output = new byte[data.Length];
|
|
|
|
using (Aes aes = Aes.Create())
|
|
{
|
|
aes.Key = key;
|
|
aes.Mode = CipherMode.ECB;
|
|
aes.Padding = PaddingMode.None;
|
|
|
|
int blockSize = aes.BlockSize / 8; // in bytes, should be 16
|
|
byte[] counter = new byte[blockSize];
|
|
Array.Copy(iv, counter, blockSize);
|
|
|
|
using (ICryptoTransform encryptor = aes.CreateEncryptor())
|
|
{
|
|
byte[] encryptedCounter = new byte[blockSize];
|
|
|
|
for (int i = 0; i < data.Length; i += blockSize)
|
|
{
|
|
// Encrypt the counter
|
|
encryptor.TransformBlock(counter, 0, blockSize, encryptedCounter, 0);
|
|
|
|
// Determine the number of bytes to process in this block
|
|
int blockLength = Math.Min(blockSize, data.Length - i);
|
|
|
|
// XOR the encrypted counter with the plaintext/ciphertext block
|
|
for (int j = 0; j < blockLength; j++)
|
|
{
|
|
output[i + j] = (byte)(data[i + j] ^ encryptedCounter[j]);
|
|
}
|
|
|
|
// Increment the counter
|
|
IncrementCounter(counter);
|
|
}
|
|
}
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
private void IncrementCounter(byte[] counter)
|
|
{
|
|
for (int i = counter.Length - 1; i >= 0; i--)
|
|
{
|
|
if (++counter[i] != 0)
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void DeriveHMACs()
|
|
{
|
|
if (isLocked)
|
|
throw new InvalidOperationException("Cannot derive HMACs when data is locked.");
|
|
|
|
// Calculate tag HMAC
|
|
byte[] tagHmacData = new byte[8 + 44];
|
|
Array.Copy(data, 0x000, tagHmacData, 0, 8);
|
|
Array.Copy(data, 0x054, tagHmacData, 8, 44);
|
|
|
|
byte[] tagHmac;
|
|
using (var hmac = new HMACSHA256(hmacTagKey))
|
|
{
|
|
tagHmac = hmac.ComputeHash(tagHmacData);
|
|
}
|
|
|
|
// Overwrite the stored tag HMAC
|
|
Array.Copy(tagHmac, 0, data, 0x034, 32);
|
|
|
|
// Prepare data for data HMAC
|
|
int len1 = 0x023; // 0x011 to 0x034 (0x034 - 0x011)
|
|
int len2 = 0x168; // 0x0A0 to 0x208 (0x208 - 0x0A0)
|
|
int len3 = tagHmac.Length; // 32 bytes
|
|
int len4 = 0x008; // 0x000 to 0x008 (0x008 - 0x000)
|
|
int len5 = 0x02C; // 0x054 to 0x080 (0x080 - 0x054)
|
|
int totalLength = len1 + len2 + len3 + len4 + len5;
|
|
byte[] dataHmacData = new byte[totalLength];
|
|
|
|
int offset = 0;
|
|
Array.Copy(data, 0x011, dataHmacData, offset, len1);
|
|
offset += len1;
|
|
Array.Copy(data, 0x0A0, dataHmacData, offset, len2);
|
|
offset += len2;
|
|
Array.Copy(tagHmac, 0, dataHmacData, offset, len3);
|
|
offset += len3;
|
|
Array.Copy(data, 0x000, dataHmacData, offset, len4);
|
|
offset += len4;
|
|
Array.Copy(data, 0x054, dataHmacData, offset, len5);
|
|
|
|
byte[] dataHmac;
|
|
using (var hmac = new HMACSHA256(hmacDataKey))
|
|
{
|
|
dataHmac = hmac.ComputeHash(dataHmacData);
|
|
}
|
|
|
|
// Overwrite the stored data HMAC
|
|
Array.Copy(dataHmac, 0, data, 0x080, 32);
|
|
}
|
|
|
|
public void VerifyHMACs()
|
|
{
|
|
if (isLocked)
|
|
throw new InvalidOperationException("Cannot verify HMACs when data is locked.");
|
|
|
|
// Calculate tag HMAC
|
|
byte[] tagHmacData = new byte[8 + 44];
|
|
Array.Copy(data, 0x000, tagHmacData, 0, 8);
|
|
Array.Copy(data, 0x054, tagHmacData, 8, 44);
|
|
|
|
byte[] calculatedTagHmac;
|
|
using (var hmac = new HMACSHA256(hmacTagKey))
|
|
{
|
|
calculatedTagHmac = hmac.ComputeHash(tagHmacData);
|
|
}
|
|
|
|
byte[] storedTagHmac = new byte[32];
|
|
Array.Copy(data, 0x034, storedTagHmac, 0, 32);
|
|
|
|
if (!calculatedTagHmac.SequenceEqual(storedTagHmac))
|
|
{
|
|
throw new Exception("Tag HMAC verification failed.");
|
|
}
|
|
|
|
// Prepare data for data HMAC
|
|
int len1 = 0x023; // 0x011 to 0x034
|
|
int len2 = 0x168; // 0x0A0 to 0x208
|
|
int len3 = calculatedTagHmac.Length; // 32 bytes
|
|
int len4 = 0x008; // 0x000 to 0x008
|
|
int len5 = 0x02C; // 0x054 to 0x080
|
|
int totalLength = len1 + len2 + len3 + len4 + len5;
|
|
byte[] dataHmacData = new byte[totalLength];
|
|
|
|
int offset = 0;
|
|
Array.Copy(data, 0x011, dataHmacData, offset, len1);
|
|
offset += len1;
|
|
Array.Copy(data, 0x0A0, dataHmacData, offset, len2);
|
|
offset += len2;
|
|
Array.Copy(calculatedTagHmac, 0, dataHmacData, offset, len3);
|
|
offset += len3;
|
|
Array.Copy(data, 0x000, dataHmacData, offset, len4);
|
|
offset += len4;
|
|
Array.Copy(data, 0x054, dataHmacData, offset, len5);
|
|
|
|
byte[] calculatedDataHmac;
|
|
using (var hmac = new HMACSHA256(hmacDataKey))
|
|
{
|
|
calculatedDataHmac = hmac.ComputeHash(dataHmacData);
|
|
}
|
|
|
|
byte[] storedDataHmac = new byte[32];
|
|
Array.Copy(data, 0x080, storedDataHmac, 0, 32);
|
|
|
|
if (!calculatedDataHmac.SequenceEqual(storedDataHmac))
|
|
{
|
|
throw new Exception("Data HMAC verification failed.");
|
|
}
|
|
}
|
|
|
|
public void Unlock()
|
|
{
|
|
if (!isLocked)
|
|
throw new InvalidOperationException("Data is already unlocked.");
|
|
|
|
// Derive keys and cipher
|
|
DeriveKeysAndCipher();
|
|
|
|
// Decrypt the encrypted data
|
|
DecryptData();
|
|
|
|
isLocked = false;
|
|
}
|
|
|
|
public void Lock()
|
|
{
|
|
if (isLocked)
|
|
throw new InvalidOperationException("Data is already locked.");
|
|
|
|
// Recalculate HMACs
|
|
DeriveHMACs();
|
|
|
|
// Encrypt the data
|
|
EncryptData();
|
|
|
|
isLocked = true;
|
|
}
|
|
|
|
public byte[] GetData()
|
|
{
|
|
return data;
|
|
}
|
|
|
|
// Property to get or set Amiibo nickname
|
|
public string AmiiboNickname
|
|
{
|
|
get
|
|
{
|
|
// data[0x020:0x034], big endian UTF-16
|
|
byte[] nicknameBytes = new byte[0x014];
|
|
Array.Copy(data, 0x020, nicknameBytes, 0, 0x014);
|
|
string nickname = System.Text.Encoding.BigEndianUnicode.GetString(nicknameBytes).TrimEnd('\0');
|
|
return nickname;
|
|
}
|
|
set
|
|
{
|
|
byte[] nicknameBytes = System.Text.Encoding.BigEndianUnicode.GetBytes(value.PadRight(10, '\0'));
|
|
if (nicknameBytes.Length > 20)
|
|
throw new ArgumentException("Nickname too long.");
|
|
Array.Copy(nicknameBytes, 0, data, 0x020, nicknameBytes.Length);
|
|
// Pad remaining bytes with zeros
|
|
for (int i = 0x020 + nicknameBytes.Length; i < 0x034; i++)
|
|
{
|
|
data[i] = 0x00;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|