using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Security.Cryptography; namespace TwlBackupBlock { /// /// TWLバックアップデータの暗号化・復号化処理を提供します。 /// public class Utility { public const int MAX_CONTENTS = 8; public enum SectionIndex { TMD, CONTENT, PUBLIC_SAVE = CONTENT + MAX_CONTENTS, SUB_BANNER, PRIVATE_SAVE }; // いまいち・・・ public const int MAX_CONTENTS_LEGACY = 1; public enum SectionIndexLegacy { TMD, CONTENT, PUBLIC_SAVE = CONTENT + MAX_CONTENTS_LEGACY, SUB_BANNER, PRIVATE_SAVE }; private const int KEY_SIZE = 16; private const int ROUND_SCALE = 16; private const int AES_MAC_SIZE = 16; private const int AES_IV_SIZE = 16; private const int AES_SIGN_HEADER_SIZE = 32; private const int BANNER_SIZE = 16 * 1024; private const int HEADER_SIZE = 240; private const int EX_HEADER_SIZE = 256; private const int LEGACY_HEADER_SIZE = 160; private const int SIGNATURE_SIZE = 1248; private const int EX_SIGNATURE_SIZE = 1280; private const int LEGACY_SIGNATURE_SIZE = 1024; private const int ENCRYPT_BANNER_SIZE = BANNER_SIZE + AES_SIGN_HEADER_SIZE; private const int ENCRYPT_HEADER_SIZE = HEADER_SIZE + AES_SIGN_HEADER_SIZE; private const int ENCRYPT_EX_HEADER_SIZE = EX_HEADER_SIZE + AES_SIGN_HEADER_SIZE; private const int ENCRYPT_LEGACY_HEADER_SIZE = LEGACY_HEADER_SIZE + AES_SIGN_HEADER_SIZE; private const int ENCRYPT_SIGNATURE_SIZE = SIGNATURE_SIZE + AES_SIGN_HEADER_SIZE; private const int ENCRYPT_EX_SIGNATURE_SIZE = EX_SIGNATURE_SIZE + AES_SIGN_HEADER_SIZE; private const int ENCRYPT_LEGACY_SIGNATURE_SIZE = LEGACY_SIGNATURE_SIZE + AES_SIGN_HEADER_SIZE; /// /// バックアップデータをブロックに分割して復号化をおこないます。 /// /// バックアップデータ。 /// データ本体の復号鍵。 /// バックアップデータの形式。 /// 分割、復号化されたブロック。 public static Blocks DecryptBackupData(byte[] data, byte[] key, BkpType type) { int headerSize = 0; int signatureSize = 0; int encryptHeaderSize = 0; int encryptSignatureSize = 0; int contentsNum = MAX_CONTENTS; int indexPublicSave = (int)SectionIndex.PUBLIC_SAVE; int indexSubBanner = (int)SectionIndex.SUB_BANNER; switch (type) { case BkpType.NORMAL: headerSize = HEADER_SIZE; signatureSize = SIGNATURE_SIZE; encryptHeaderSize = ENCRYPT_HEADER_SIZE; encryptSignatureSize = ENCRYPT_SIGNATURE_SIZE; break; case BkpType.WITH_PRIVATE_SAVE: headerSize = EX_HEADER_SIZE; signatureSize = EX_SIGNATURE_SIZE; encryptHeaderSize = ENCRYPT_EX_HEADER_SIZE; encryptSignatureSize = ENCRYPT_EX_SIGNATURE_SIZE; break; case BkpType.LEGACY: headerSize = LEGACY_HEADER_SIZE; signatureSize = LEGACY_SIGNATURE_SIZE; encryptHeaderSize = ENCRYPT_LEGACY_HEADER_SIZE; encryptSignatureSize = ENCRYPT_LEGACY_SIGNATURE_SIZE; contentsNum = MAX_CONTENTS_LEGACY; indexPublicSave = (int)SectionIndexLegacy.PUBLIC_SAVE; indexSubBanner = (int)SectionIndexLegacy.SUB_BANNER; break; } if (data == null) { throw new ArgumentNullException("data"); } if (key == null) { throw new ArgumentNullException("key"); } { int minDataSize = ENCRYPT_BANNER_SIZE + encryptHeaderSize + encryptSignatureSize; if (data.Length < minDataSize) { string message = string.Format("data.Length < {0} (data.Length:{1})", data, minDataSize); throw new ArgumentException("data", message); } } if (key.Length != KEY_SIZE) { string message = string.Format("key.Length != {0} (key.Length:{1})", KEY_SIZE, key.Length); throw new ArgumentException("key", message); } Blocks blocks = new Blocks(); using (var ms = new MemoryStream(data)) using (var br = new BinaryReader(ms)) { blocks.banner.body = new Body(DecryptBlock(br, BANNER_SIZE, key, out blocks.banner.mac, out blocks.banner.iv)); HeaderBody headerBody = new HeaderBody( DecryptBlock(br, headerSize, key, out blocks.header.mac, out blocks.header.iv), type); blocks.header.body = headerBody; if (!CheckSignature(headerBody)) { throw new Exception("ヘッダブロックのシグネチャが正しくありません"); } blocks.signature.body = new SignatureBody( DecryptBlock(br, signatureSize, key, out blocks.signature.mac, out blocks.signature.iv), type); int TMD_SIZE = (int)headerBody.fileSizes[(int)SectionIndex.TMD]; int[] CONTENT_SIZE = new int[contentsNum]; for (int i = 0; i < contentsNum; i++) { CONTENT_SIZE[i] = (int)headerBody.fileSizes[(int)SectionIndex.CONTENT + i]; } int PUBLIC_SAVE_SIZE = (int)headerBody.fileSizes[indexPublicSave]; int SUB_BANNER_SIZE = (int)headerBody.fileSizes[indexSubBanner]; int PRIVATE_SAVE_SIZE = 0; if (type == BkpType.WITH_PRIVATE_SAVE) { PRIVATE_SAVE_SIZE = (int)headerBody.privateSaveForWPS; } blocks.tmd.body = new Body(DecryptBlock(br, TMD_SIZE, key, out blocks.tmd.mac, out blocks.tmd.iv)); for (int i = 0; i < contentsNum; i++) { if (CONTENT_SIZE[i] > 0) { blocks.content[i].body = new Body(DecryptBlock(br, CONTENT_SIZE[i], key, out blocks.content[i].mac, out blocks.content[i].iv)); } else { blocks.content[i].body = null; } } blocks.publicSave.body = new Body(DecryptBlock(br, PUBLIC_SAVE_SIZE, key, out blocks.publicSave.mac, out blocks.publicSave.iv)); blocks.subBanner.body = new Body(DecryptBlock(br, SUB_BANNER_SIZE, key, out blocks.subBanner.mac, out blocks.subBanner.iv)); if (type == BkpType.WITH_PRIVATE_SAVE) { blocks.privateSave.body = new Body(DecryptBlock(br, PRIVATE_SAVE_SIZE, key, out blocks.privateSave.mac, out blocks.privateSave.iv)); } Debug.Assert(ms.Position == data.Length); } return blocks; } /// /// ヘッダのデータ本体のシグネチャが正しいか判定します。 /// /// ヘッダのデータ本体。 /// シグネチャが正しい場合はtrue。それ以外の場合はfalse。 private static bool CheckSignature(HeaderBody header) { byte[] signature = { (byte)'3', (byte)'F', (byte)'D', (byte)'T' }; byte[] bytes = BitConverter.GetBytes(header.signature); Debug.Assert(signature.Length == bytes.Length); for (int i = 0; i < bytes.Length; i++) { if (bytes[i] != signature[i]) { return false; } } return true; } /// /// binaryReaderから読み込んだブロックを復号化します。 /// /// ブロックを読み出すBinaryReader。 /// 暗号化前のブロックのデータ本体のバイトサイズ。 /// データ本体の復号鍵。 /// ブロックのMAC。 /// ブロックのIV。 /// ブロックのデータ本体。 private static byte[] DecryptBlock(BinaryReader binaryReader, int bodySize, byte[] key, out byte[] mac, out byte[] iv) { Debug.Assert(key.Length == KEY_SIZE); int encryptBodySize = RoundUp(bodySize, ROUND_SCALE); byte[] body = binaryReader.ReadBytes(encryptBodySize); mac = binaryReader.ReadBytes(AES_MAC_SIZE); iv = binaryReader.ReadBytes(AES_IV_SIZE); return DecryptBody(body, key, iv, bodySize); } /// /// ブロックのデータ本体を復号化します。 /// /// 暗号化されたデータ本体。 /// データ本体の復号鍵。 /// 暗号化に利用したIV。 /// 暗号化前のブロックのデータ本体のバイトサイズ。 /// 復号化されたデータ本体。 public static byte[] DecryptBody(byte[] body, byte[] key, byte[] iv, int size) { if (body == null) { throw new ArgumentNullException("body"); } if (key == null) { throw new ArgumentNullException("key"); } if (iv == null) { throw new ArgumentNullException("iv"); } if (body.Length % ROUND_SCALE != 0) { string message = string.Format("body.Length % {0} != 0 (body.Length:{1})", ROUND_SCALE, body.Length); throw new ArgumentException("body", message); } if (key.Length != KEY_SIZE) { string message = string.Format("key.Length != {0} (key.Length:{1})", KEY_SIZE, key.Length); throw new ArgumentException("key", message); } if (iv.Length != AES_IV_SIZE) { string message = string.Format("iv.Length != {0} (iv.Length:{1})", AES_IV_SIZE, iv.Length); throw new ArgumentException("iv", message); } if (size < 0) { string message = string.Format("size < 0 (size:{0})", size); throw new ArgumentException("size", message); } if (size > body.Length) { string message = string.Format("size > body.Length (size:{0}, body.Length:{1})", size, body.Length); throw new ArgumentException("size", message); } byte[] result = new byte[size]; using (var ms = new MemoryStream(body)) { RijndaelManaged aes = new RijndaelManaged(); aes.Mode = CipherMode.CBC; aes.Padding = PaddingMode.Zeros; aes.Key = key; aes.IV = iv; using (CryptoStream cs = new CryptoStream(ms, aes.CreateDecryptor(), CryptoStreamMode.Read)) { cs.Read(result, 0, result.Length); } } return result; } /// /// ブロックのデータ本体を暗号化します。 /// /// 復号化されたデータ本体。 /// データ本体の暗号鍵。 /// 暗号化に利用するIV。 /// 暗号化されたデータ本体。 public static byte[] EncryptBody(byte[] body, byte[] key, byte[] iv) { if (body == null) { throw new ArgumentNullException("body"); } if (key == null) { throw new ArgumentNullException("key"); } if (iv == null) { throw new ArgumentNullException("iv"); } if (key.Length != KEY_SIZE) { string message = string.Format("key.Length != {0} (key.Length:{1})", KEY_SIZE, key.Length); throw new ArgumentException("key", message); } if (iv.Length != AES_IV_SIZE) { string message = string.Format("iv.Length != {0} (iv.Length:{1})", AES_IV_SIZE, iv.Length); throw new ArgumentException("iv", message); } byte[] result = new byte[RoundUp(body.Length, ROUND_SCALE)]; using (var ms = new MemoryStream(body)) { RijndaelManaged aes = new RijndaelManaged(); aes.Mode = CipherMode.CBC; aes.Padding = PaddingMode.Zeros; aes.Key = key; aes.IV = iv; using (CryptoStream cs = new CryptoStream(ms, aes.CreateEncryptor(), CryptoStreamMode.Read)) { cs.Read(result, 0, result.Length); } } return result; } /// /// ブロックのデータ本体を暗号化します。内部的にはEncryptBody(body.GetBytes(), key, iv)を呼び出します。 /// /// 暗号化されたデータ本体。 /// データ本体の暗号鍵。 /// 暗号化に利用するIV。 /// 暗号化されたデータ本体。 public static byte[] EncryptBody(AbstractBody body, byte[] key, byte[] iv) { if (body == null) { throw new ArgumentNullException("body"); } return EncryptBody(body.GetBytes(), key, iv); } /// /// ブロックのデータ本体からハッシュを取得します。 /// /// 暗号化前のデータ本体。 /// 32バイトのハッシュ。 public static byte[] GetSha256(byte[] data) { if (data == null) { throw new ArgumentNullException("data"); } var sha = new SHA256Managed(); byte[] buf = Enumerable.Repeat(0, RoundUp(data.Length, ROUND_SCALE)).ToArray(); Array.Copy(data, 0, buf, 0, data.Length); return sha.ComputeHash(buf); } /// /// ハッシュからMACを取得します。 /// /// データ本体のハッシュ。 /// AES-CMACの鍵。 /// 16バイトのMAC。 public static byte[] GetAesCmac(byte[] data, byte[] key) { return AesCmac.GetMac(data, key); } /// /// 基準値の倍数のうち、指定された整数以上で最小のものを返します。 /// /// 整数。 /// 基準値。 /// yの倍数のうち、x以上で最小の整数。 private static int RoundUp(int x, int y) { return (x + y - 1) / y * y; } /// /// インポートシーケンスと同様の手順でバックアップデータを検証します。 /// ただし、このメソッドでは署名データの検証はおこないません。 /// 署名以外の整合性がとれており、署名の整合性がとれていない場合、このメソッドはtrueを返します。 /// /// 検証にかけるバックアップデータ。 /// データ本体の暗号鍵。 /// MACの鍵。 /// バックアップの形式。 /// 検証に成功した場合はtrue。それ以外の場合はfalse。 public static bool Verify(byte[] data, byte[] aesCbcKey, byte[] aesCmacKey, BkpType type) { int headerSize = 0; int signatureSize = 0; int encryptHeaderSize = 0; int encryptSignatureSize = 0; int contentsNum = MAX_CONTENTS; int indexPublicSave = (int)SectionIndex.PUBLIC_SAVE; int indexSubBanner = (int)SectionIndex.SUB_BANNER; switch (type) { case BkpType.NORMAL: headerSize = HEADER_SIZE; signatureSize = SIGNATURE_SIZE; encryptHeaderSize = ENCRYPT_HEADER_SIZE; encryptSignatureSize = ENCRYPT_SIGNATURE_SIZE; break; case BkpType.WITH_PRIVATE_SAVE: headerSize = EX_HEADER_SIZE; signatureSize = EX_SIGNATURE_SIZE; encryptHeaderSize = ENCRYPT_EX_HEADER_SIZE; encryptSignatureSize = ENCRYPT_EX_SIGNATURE_SIZE; break; case BkpType.LEGACY: headerSize = LEGACY_HEADER_SIZE; signatureSize = LEGACY_SIGNATURE_SIZE; encryptHeaderSize = ENCRYPT_LEGACY_HEADER_SIZE; encryptSignatureSize = ENCRYPT_LEGACY_SIGNATURE_SIZE; contentsNum = MAX_CONTENTS_LEGACY; indexPublicSave = (int)SectionIndexLegacy.PUBLIC_SAVE; indexSubBanner = (int)SectionIndexLegacy.SUB_BANNER; break; } if (data == null) { throw new ArgumentNullException("data"); } if (aesCbcKey == null) { throw new ArgumentNullException("key"); } if (aesCbcKey.Length != KEY_SIZE) { string message = string.Format("key.Length != {0} (key.Length:{1})", KEY_SIZE, aesCbcKey.Length); throw new ArgumentException("key", message); } { int minDataSize = ENCRYPT_BANNER_SIZE + encryptHeaderSize + encryptSignatureSize; if (data.Length < minDataSize) { Console.WriteLine("バックアップファイルサイズがバナー+ヘッダ+署名ブロック未満です"); return false; } } Blocks blocks = new Blocks(); using (var ms = new MemoryStream(data)) using (var br = new BinaryReader(ms)) { blocks.banner.body = new Body(DecryptBlock(br, BANNER_SIZE, aesCbcKey, out blocks.banner.mac, out blocks.banner.iv)); HeaderBody headerBody = new HeaderBody( DecryptBlock(br, headerSize, aesCbcKey, out blocks.header.mac, out blocks.header.iv), type); blocks.header.body = headerBody; SignatureBody signatureBody = new SignatureBody( DecryptBlock(br, signatureSize, aesCbcKey, out blocks.signature.mac, out blocks.signature.iv), type); blocks.signature.body = signatureBody; if (!CheckMac(blocks.header, aesCmacKey, blocks.header.mac)) { Console.WriteLine("ヘッダブロックのMACが一致しません"); return false; } if (!CheckSignature(headerBody)) { Console.WriteLine("ヘッダブロックのシグネチャが正しくありません"); return false; } if (!CheckMac(blocks.signature, aesCmacKey, blocks.signature.mac)) { Console.WriteLine("署名ブロックのMACが一致しません"); return false; } /* 署名データの検証 */ if (!CheckHash(blocks.header, signatureBody.digest.header)) { Console.WriteLine("ヘッダブロックのハッシュが一致しません"); return false; } if (!CheckMac(blocks.banner, aesCmacKey, blocks.banner.mac)) { Console.WriteLine("バナーブロックのMACが一致しません"); return false; } if (!CheckHash(blocks.banner, signatureBody.digest.banner)) { Console.WriteLine("バナーブロックのハッシュが一致しません"); return false; } int TMD_SIZE = (int)headerBody.fileSizes[(int)SectionIndex.TMD]; int[] CONTENT_SIZE = new int[contentsNum]; for (int i = 0; i < contentsNum; i++) { CONTENT_SIZE[i] = (int)headerBody.fileSizes[(int)SectionIndex.CONTENT + i]; } int PUBLIC_SAVE_SIZE = (int)headerBody.fileSizes[indexPublicSave]; int SUB_BANNER_SIZE = (int)headerBody.fileSizes[indexSubBanner]; int PRIVATE_SAVE_SIZE = 0; if (type == BkpType.WITH_PRIVATE_SAVE) { PRIVATE_SAVE_SIZE = (int)headerBody.privateSaveForWPS; } int restSize = RoundUp(TMD_SIZE, ROUND_SCALE) + RoundUp(PUBLIC_SAVE_SIZE, ROUND_SCALE) + RoundUp(SUB_BANNER_SIZE, ROUND_SCALE) + AES_SIGN_HEADER_SIZE * 3; for (int i = 0; i < contentsNum; i++) { if (CONTENT_SIZE[i] > 0) { restSize += RoundUp(CONTENT_SIZE[i], ROUND_SCALE) + AES_SIGN_HEADER_SIZE; } } if (type == BkpType.WITH_PRIVATE_SAVE) { restSize += RoundUp(PRIVATE_SAVE_SIZE, ROUND_SCALE) + AES_SIGN_HEADER_SIZE; } if (data.Length - ms.Position != restSize) { Console.WriteLine("TMD、コンテンツ、publicセーブデータ、サブバナーブロックサイズが実際のサイズと一致しません"); return false; } blocks.tmd.body = new Body(DecryptBlock(br, TMD_SIZE, aesCbcKey, out blocks.tmd.mac, out blocks.tmd.iv)); if (!CheckMac(blocks.tmd, aesCmacKey, blocks.tmd.mac)) { Console.WriteLine("TMDブロックのMACが一致しません"); return false; } if (!CheckHash(blocks.tmd, signatureBody.digest.section[(int)SectionIndex.TMD])) { Console.WriteLine("TMDブロックのハッシュが一致しません"); return false; } for (int i = 0; i < contentsNum; i++) { if (CONTENT_SIZE[i] > 0) { blocks.content[i].body = new Body(DecryptBlock(br, CONTENT_SIZE[i], aesCbcKey, out blocks.content[i].mac, out blocks.content[i].iv)); if (!CheckMac(blocks.content[i], aesCmacKey, blocks.content[i].mac)) { Console.WriteLine("コンテンツブロック[{0}]のMACが一致しません", i); return false; } if (!CheckHash(blocks.content[i], signatureBody.digest.section[(int)SectionIndex.CONTENT + i])) { Console.WriteLine("コンテンツブロック[{0}]のハッシュが一致しません", i); return false; } } } blocks.publicSave.body = new Body(DecryptBlock(br, PUBLIC_SAVE_SIZE, aesCbcKey, out blocks.publicSave.mac, out blocks.publicSave.iv)); if (!CheckMac(blocks.publicSave, aesCmacKey, blocks.publicSave.mac)) { Console.WriteLine("publicセーブデータブロックのMACが一致しません"); return false; } if (!CheckHash(blocks.publicSave, signatureBody.digest.section[indexPublicSave])) { Console.WriteLine("publicセーブデータブロックのハッシュが一致しません"); return false; } blocks.subBanner.body = new Body(DecryptBlock(br, SUB_BANNER_SIZE, aesCbcKey, out blocks.subBanner.mac, out blocks.subBanner.iv)); if (!CheckMac(blocks.subBanner, aesCmacKey, blocks.subBanner.mac)) { Console.WriteLine("サブバナーブロックの内容とMACが一致しません"); return false; } if (!CheckHash(blocks.subBanner, signatureBody.digest.section[indexSubBanner])) { Console.WriteLine("サブバナーブロックのハッシュが一致しません"); return false; } // WITH_PRIVATE_SAVE の時は private save も対象 if (type == BkpType.WITH_PRIVATE_SAVE) { blocks.privateSave.body = new Body(DecryptBlock(br, PRIVATE_SAVE_SIZE, aesCbcKey, out blocks.privateSave.mac, out blocks.privateSave.iv)); if (!CheckMac(blocks.privateSave, aesCmacKey, blocks.privateSave.mac)) { Console.WriteLine("privateセーブデータブロックの内容とMACが一致しません"); return false; } if (!CheckHash(blocks.privateSave, signatureBody.digest.section[(int)SectionIndex.PRIVATE_SAVE])) { Console.WriteLine("privateセーブデータブロックのハッシュが一致しません"); return false; } } Debug.Assert(ms.Position == data.Length); } return true; } /// /// ハッシュによりブロックが正しいか判定します。 /// /// ブロック。 /// ハッシュ。 /// ハッシュがブロックから生成したものと一致した場合はtrue。それ以外の場合はfalse。 private static bool CheckHash(Block block, byte[] hash) { byte[] tryHash = GetSha256(block.body.GetBytes()); Debug.Assert(tryHash.Length == hash.Length); for (int i = 0; i < tryHash.Length; i++) { if (tryHash[i] != hash[i]) { return false; } } return true; } /// /// MACによりブロックが正しいか判定します。 /// /// ブロック。 /// MACの鍵。 /// MAC。 /// MACがブロックから生成したものと一致した場合はtrue。それ以外の場合はfalse。 private static bool CheckMac(Block block, byte[] key, byte[] mac) { byte[] tryMac = GetAesCmac(GetSha256(block.body.GetBytes()), key); Debug.Assert(tryMac.Length == mac.Length); for (int i = 0; i < tryMac.Length; ++i) { if (tryMac[i] != mac[i]) { return false; } } return true; } } }