Jacobwasbeast 0adaa4cb96
Adds the ability to read and write to amiibo bin files (#348)
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.
2024-12-19 22:36:46 -06:00

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;
}
}
}
}
}