From 11897379274752ca0f223b1d35a56cc1b9a29557 Mon Sep 17 00:00:00 2001 From: roblabla Date: Sat, 22 Dec 2018 18:21:27 +0000 Subject: [PATCH 01/10] PKI: Allow creating a dev keyset. --- src/pki.rs | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/src/pki.rs b/src/pki.rs index c1931be..a3d11e1 100644 --- a/src/pki.rs +++ b/src/pki.rs @@ -315,6 +315,65 @@ impl Keys { )) } + pub fn new_dev(key_path: Option<&Path>) -> Result { + Keys::new(key_path, Path::new("dev.keys"), ( + /* nca_hdr_fixed_key_modulus: */ Modulus([ + 0xD8, 0xF1, 0x18, 0xEF, 0x32, 0x72, 0x4C, 0xA7, 0x47, 0x4C, 0xB9, 0xEA, 0xB3, 0x04, 0xA8, 0xA4, + 0xAC, 0x99, 0x08, 0x08, 0x04, 0xBF, 0x68, 0x57, 0xB8, 0x43, 0x94, 0x2B, 0xC7, 0xB9, 0x66, 0x49, + 0x85, 0xE5, 0x8A, 0x9B, 0xC1, 0x00, 0x9A, 0x6A, 0x8D, 0xD0, 0xEF, 0xCE, 0xFF, 0x86, 0xC8, 0x5C, + 0x5D, 0xE9, 0x53, 0x7B, 0x19, 0x2A, 0xA8, 0xC0, 0x22, 0xD1, 0xF3, 0x22, 0x0A, 0x50, 0xF2, 0x2B, + 0x65, 0x05, 0x1B, 0x9E, 0xEC, 0x61, 0xB5, 0x63, 0xA3, 0x6F, 0x3B, 0xBA, 0x63, 0x3A, 0x53, 0xF4, + 0x49, 0x2F, 0xCF, 0x03, 0xCC, 0xD7, 0x50, 0x82, 0x1B, 0x29, 0x4F, 0x08, 0xDE, 0x1B, 0x6D, 0x47, + 0x4F, 0xA8, 0xB6, 0x6A, 0x26, 0xA0, 0x83, 0x3F, 0x1A, 0xAF, 0x83, 0x8F, 0x0E, 0x17, 0x3F, 0xFE, + 0x44, 0x1C, 0x56, 0x94, 0x2E, 0x49, 0x83, 0x83, 0x03, 0xE9, 0xB6, 0xAD, 0xD5, 0xDE, 0xE3, 0x2D, + 0xA1, 0xD9, 0x66, 0x20, 0x5D, 0x1F, 0x5E, 0x96, 0x5D, 0x5B, 0x55, 0x0D, 0xD4, 0xB4, 0x77, 0x6E, + 0xAE, 0x1B, 0x69, 0xF3, 0xA6, 0x61, 0x0E, 0x51, 0x62, 0x39, 0x28, 0x63, 0x75, 0x76, 0xBF, 0xB0, + 0xD2, 0x22, 0xEF, 0x98, 0x25, 0x02, 0x05, 0xC0, 0xD7, 0x6A, 0x06, 0x2C, 0xA5, 0xD8, 0x5A, 0x9D, + 0x7A, 0xA4, 0x21, 0x55, 0x9F, 0xF9, 0x3E, 0xBF, 0x16, 0xF6, 0x07, 0xC2, 0xB9, 0x6E, 0x87, 0x9E, + 0xB5, 0x1C, 0xBE, 0x97, 0xFA, 0x82, 0x7E, 0xED, 0x30, 0xD4, 0x66, 0x3F, 0xDE, 0xD8, 0x1B, 0x4B, + 0x15, 0xD9, 0xFB, 0x2F, 0x50, 0xF0, 0x9D, 0x1D, 0x52, 0x4C, 0x1C, 0x4D, 0x8D, 0xAE, 0x85, 0x1E, + 0xEA, 0x7F, 0x86, 0xF3, 0x0B, 0x7B, 0x87, 0x81, 0x98, 0x23, 0x80, 0x63, 0x4F, 0x2F, 0xB0, 0x62, + 0xCC, 0x6E, 0xD2, 0x46, 0x13, 0x65, 0x2B, 0xD6, 0x44, 0x33, 0x59, 0xB5, 0x8F, 0xB9, 0x4A, 0xA9 + ]), + /* acid_fixed_key_modulus: */ Modulus([ + 0xD6, 0x34, 0xA5, 0x78, 0x6C, 0x68, 0xCE, 0x5A, 0xC2, 0x37, 0x17, 0xF3, 0x82, 0x45, 0xC6, 0x89, + 0xE1, 0x2D, 0x06, 0x67, 0xBF, 0xB4, 0x06, 0x19, 0x55, 0x6B, 0x27, 0x66, 0x0C, 0xA4, 0xB5, 0x87, + 0x81, 0x25, 0xF4, 0x30, 0xBC, 0x53, 0x08, 0x68, 0xA2, 0x48, 0x49, 0x8C, 0x3F, 0x38, 0x40, 0x9C, + 0xC4, 0x26, 0xF4, 0x79, 0xE2, 0xA1, 0x85, 0xF5, 0x5C, 0x7F, 0x58, 0xBA, 0xA6, 0x1C, 0xA0, 0x8B, + 0x84, 0x16, 0x14, 0x6F, 0x85, 0xD9, 0x7C, 0xE1, 0x3C, 0x67, 0x22, 0x1E, 0xFB, 0xD8, 0xA7, 0xA5, + 0x9A, 0xBF, 0xEC, 0x0E, 0xCF, 0x96, 0x7E, 0x85, 0xC2, 0x1D, 0x49, 0x5D, 0x54, 0x26, 0xCB, 0x32, + 0x7C, 0xF6, 0xBB, 0x58, 0x03, 0x80, 0x2B, 0x5D, 0xF7, 0xFB, 0xD1, 0x9D, 0xC7, 0xC6, 0x2E, 0x53, + 0xC0, 0x6F, 0x39, 0x2C, 0x1F, 0xA9, 0x92, 0xF2, 0x4D, 0x7D, 0x4E, 0x74, 0xFF, 0xE4, 0xEF, 0xE4, + 0x7C, 0x3D, 0x34, 0x2A, 0x71, 0xA4, 0x97, 0x59, 0xFF, 0x4F, 0xA2, 0xF4, 0x66, 0x78, 0xD8, 0xBA, + 0x99, 0xE3, 0xE6, 0xDB, 0x54, 0xB9, 0xE9, 0x54, 0xA1, 0x70, 0xFC, 0x05, 0x1F, 0x11, 0x67, 0x4B, + 0x26, 0x8C, 0x0C, 0x3E, 0x03, 0xD2, 0xA3, 0x55, 0x5C, 0x7D, 0xC0, 0x5D, 0x9D, 0xFF, 0x13, 0x2F, + 0xFD, 0x19, 0xBF, 0xED, 0x44, 0xC3, 0x8C, 0xA7, 0x28, 0xCB, 0xE5, 0xE0, 0xB1, 0xA7, 0x9C, 0x33, + 0x8D, 0xB8, 0x6E, 0xDE, 0x87, 0x18, 0x22, 0x60, 0xC4, 0xAE, 0xF2, 0x87, 0x9F, 0xCE, 0x09, 0x5C, + 0xB5, 0x99, 0xA5, 0x9F, 0x49, 0xF2, 0xD7, 0x58, 0xFA, 0xF9, 0xC0, 0x25, 0x7D, 0xD6, 0xCB, 0xF3, + 0xD8, 0x6C, 0xA2, 0x69, 0x91, 0x68, 0x73, 0xB1, 0x94, 0x6F, 0xA3, 0xF3, 0xB9, 0x7D, 0xF8, 0xE0, + 0x72, 0x9E, 0x93, 0x7B, 0x7A, 0xA2, 0x57, 0x60, 0xB7, 0x5B, 0xA9, 0x84, 0xAE, 0x64, 0x88, 0x69 + ]), + /* package2_fixed_key_modulus: */ Modulus([ + 0xB3, 0x65, 0x54, 0xFB, 0x0A, 0xB0, 0x1E, 0x85, 0xA7, 0xF6, 0xCF, 0x91, 0x8E, 0xBA, 0x96, 0x99, + 0x0D, 0x8B, 0x91, 0x69, 0x2A, 0xEE, 0x01, 0x20, 0x4F, 0x34, 0x5C, 0x2C, 0x4F, 0x4E, 0x37, 0xC7, + 0xF1, 0x0B, 0xD4, 0xCD, 0xA1, 0x7F, 0x93, 0xF1, 0x33, 0x59, 0xCE, 0xB1, 0xE9, 0xDD, 0x26, 0xE6, + 0xF3, 0xBB, 0x77, 0x87, 0x46, 0x7A, 0xD6, 0x4E, 0x47, 0x4A, 0xD1, 0x41, 0xB7, 0x79, 0x4A, 0x38, + 0x06, 0x6E, 0xCF, 0x61, 0x8F, 0xCD, 0xC1, 0x40, 0x0B, 0xFA, 0x26, 0xDC, 0xC0, 0x34, 0x51, 0x83, + 0xD9, 0x3B, 0x11, 0x54, 0x3B, 0x96, 0x27, 0x32, 0x9A, 0x95, 0xBE, 0x1E, 0x68, 0x11, 0x50, 0xA0, + 0x6B, 0x10, 0xA8, 0x83, 0x8B, 0xF5, 0xFC, 0xBC, 0x90, 0x84, 0x7A, 0x5A, 0x5C, 0x43, 0x52, 0xE6, + 0xC8, 0x26, 0xE9, 0xFE, 0x06, 0xA0, 0x8B, 0x53, 0x0F, 0xAF, 0x1E, 0xC4, 0x1C, 0x0B, 0xCF, 0x50, + 0x1A, 0xA4, 0xF3, 0x5C, 0xFB, 0xF0, 0x97, 0xE4, 0xDE, 0x32, 0x0A, 0x9F, 0xE3, 0x5A, 0xAA, 0xB7, + 0x44, 0x7F, 0x5C, 0x33, 0x60, 0xB9, 0x0F, 0x22, 0x2D, 0x33, 0x2A, 0xE9, 0x69, 0x79, 0x31, 0x42, + 0x8F, 0xE4, 0x3A, 0x13, 0x8B, 0xE7, 0x26, 0xBD, 0x08, 0x87, 0x6C, 0xA6, 0xF2, 0x73, 0xF6, 0x8E, + 0xA7, 0xF2, 0xFE, 0xFB, 0x6C, 0x28, 0x66, 0x0D, 0xBD, 0xD7, 0xEB, 0x42, 0xA8, 0x78, 0xE6, 0xB8, + 0x6B, 0xAE, 0xC7, 0xA9, 0xE2, 0x40, 0x6E, 0x89, 0x20, 0x82, 0x25, 0x8E, 0x3C, 0x6A, 0x60, 0xD7, + 0xF3, 0x56, 0x8E, 0xEC, 0x8D, 0x51, 0x8A, 0x63, 0x3C, 0x04, 0x78, 0x23, 0x0E, 0x90, 0x0C, 0xB4, + 0xE7, 0x86, 0x3B, 0x4F, 0x8E, 0x13, 0x09, 0x47, 0x32, 0x0E, 0x04, 0xB8, 0x4D, 0x5B, 0xB0, 0x46, + 0x71, 0xB0, 0x5C, 0xF4, 0xAD, 0x63, 0x4F, 0xC5, 0xE2, 0xAC, 0x1E, 0xC4, 0x33, 0x96, 0x09, 0x7B + ]) + )) + } + fn read_from_ini(&mut self, mut file: File) -> Result<(), Error> { let config = ini::Ini::read_from(&mut file)?; let section = config.general_section(); From 65257f3bf84661316966a1988dcf01102908dc78 Mon Sep 17 00:00:00 2001 From: roblabla Date: Sat, 22 Dec 2018 18:24:23 +0000 Subject: [PATCH 02/10] PKI: Make keys public so they may be used in other modules. --- Cargo.lock | 41 ++++++++++++++++++++++ Cargo.toml | 1 + src/lib.rs | 3 +- src/pki.rs | 98 +++++++++++++++++++++++++++------------------------- src/utils.rs | 78 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 172 insertions(+), 49 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ca3465a..b4c7303 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -332,6 +332,16 @@ dependencies = [ "typenum 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "getset" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.13.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "glob" version = "0.2.11" @@ -388,6 +398,7 @@ dependencies = [ "dirs 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", "elf 0.0.10 (registry+https://github.com/rust-lang/crates.io-index)", "failure 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "getset 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)", "goblin 0.0.17 (registry+https://github.com/rust-lang/crates.io-index)", "lz4 1.22.0 (git+https://github.com/bozaro/lz4-rs.git)", "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", @@ -458,6 +469,14 @@ name = "plain" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "proc-macro2" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "proc-macro2" version = "0.4.24" @@ -474,6 +493,14 @@ dependencies = [ "bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "quote" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "quote" version = "0.6.4" @@ -663,6 +690,16 @@ dependencies = [ "syn 0.15.22 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "syn" +version = "0.13.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "syn" version = "0.14.7" @@ -849,6 +886,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" "checksum generic-array 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3c0f28c2f5bfb5960175af447a2da7c18900693738343dc896ffbcabd9839592" "checksum generic-array 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ef25c5683767570c2bbd7deba372926a55eaae9982d7726ee2a1050239d45b9d" +"checksum getset 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "54c7f36a235738bb25904d6a2b3dbb28f6f5736cd3918c4bf80d6bb236200782" "checksum glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb" "checksum goblin 0.0.17 (registry+https://github.com/rust-lang/crates.io-index)" = "5911d7df7b8f65ab676c5327b50acea29d3c6a1a4ad05e444cf5dce321b26db2" "checksum heck 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ea04fa3ead4e05e51a7c806fc07271fdbde4e246a6c6d1efd52e72230b771b82" @@ -864,8 +902,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum opaque-debug 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "51ecbcb821e1bd256d456fe858aaa7f380b63863eab2eb86eee1bd9f33dd6682" "checksum percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" "checksum plain 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +"checksum proc-macro2 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "1b06e2f335f48d24442b35a19df506a835fb3547bc3c06ef27340da9acf5cae7" "checksum proc-macro2 0.4.24 (registry+https://github.com/rust-lang/crates.io-index)" = "77619697826f31a02ae974457af0b29b723e5619e113e9397b8b82c6bd253f09" "checksum pulldown-cmark 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d6fdf85cda6cadfae5428a54661d431330b312bc767ddbc57adbedc24da66e32" +"checksum quote 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9949cfe66888ffe1d53e6ec9d9f3b70714083854be20fd5e271b232a017401e8" "checksum quote 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b71f9f575d55555aa9c06188be9d4e2bfc83ed02537948ac0d520c24d0419f1a" "checksum rand 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "8356f47b32624fef5b3301c1be97e5944ecdd595409cc5da11d05f211db6cfbd" "checksum redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "c214e91d3ecf43e9a4e41e578973adeb14b474f2bee858742d127af75a0112b1" @@ -889,6 +929,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb4f380125926a99e52bc279241539c018323fab05ad6368b56f93d9369ff550" "checksum structopt 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)" = "41c4a2479a078509940d82773d90ff824a8c89533ab3b59cd3ce8b0c0e369c02" "checksum structopt-derive 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)" = "5352090cfae7a2c85e1a31146268b53396106c88ca5d6ccee2e3fae83b6e35c2" +"checksum syn 0.13.11 (registry+https://github.com/rust-lang/crates.io-index)" = "14f9bf6292f3a61d2c716723fdb789a41bbe104168e6f496dc6497e531ea1b9b" "checksum syn 0.14.7 (registry+https://github.com/rust-lang/crates.io-index)" = "e2e13df71f29f9440b50261a5882c86eac334f1badb3134ec26f0de2f1418e44" "checksum syn 0.15.22 (registry+https://github.com/rust-lang/crates.io-index)" = "ae8b29eb5210bc5cf63ed6149cbf9adfc82ac0be023d8735c176ee74a2db4da7" "checksum synstructure 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "73687139bf99285483c96ac0add482c3776528beac1d97d444f6e91f203a2015" diff --git a/Cargo.toml b/Cargo.toml index 979195f..629534f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ goblin = { version = "0.0.17", optional = true } rust-ini = "0.13" failure = "0.1" dirs = "1.0" +getset = "0.0.6" block-modes = "0.2" aes = "0.3" num-traits = "0.2" diff --git a/src/lib.rs b/src/lib.rs index e3e1991..dfc8c68 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ +#[macro_use] +mod utils; pub mod format; pub mod pki; pub mod error; -mod utils; diff --git a/src/pki.rs b/src/pki.rs index a3d11e1..a1d60cc 100644 --- a/src/pki.rs +++ b/src/pki.rs @@ -10,12 +10,7 @@ use block_modes::{Ctr128, BlockModeIv, BlockMode}; use block_modes::block_padding::ZeroPadding; use aes::block_cipher_trait::generic_array::GenericArray; use aes::block_cipher_trait::BlockCipher; - -struct Aes128Key([u8; 0x10]); -struct AesXtsKey([u8; 0x20]); -struct EncryptedKeyblob([u8; 0xB0]); -struct Keyblob([u8; 0x90]); -struct Modulus([u8; 0x100]); +use getset::Getters; macro_rules! impl_debug { ($for:ident) => { @@ -35,6 +30,13 @@ impl_debug!(AesXtsKey); impl_debug!(EncryptedKeyblob); impl_debug!(Keyblob); impl_debug!(Modulus); +#[derive(Clone, Copy)] +pub struct Aes128Key([u8; 0x10]); +#[derive(Clone, Copy)] +pub struct AesXtsKey([u8; 0x20]); +pub struct EncryptedKeyblob([u8; 0xB0]); +pub struct Keyblob([u8; 0x90]); +pub struct Modulus([u8; 0x100]); impl EncryptedKeyblob { fn decrypt(&self, key: &Aes128Key) -> Result { @@ -49,7 +51,7 @@ impl EncryptedKeyblob { } impl Aes128Key { - fn derive_key(&self, source: &[u8; 0x10]) -> Result { + pub fn derive_key(&self, source: &[u8; 0x10]) -> Result { let mut newkey = *source; let crypter = Aes128::new(GenericArray::from_slice(&self.0)); @@ -58,7 +60,7 @@ impl Aes128Key { Ok(Aes128Key(newkey)) } - fn derive_xts_key(&self, source: &[u8; 0x20]) -> Result { + pub fn derive_xts_key(&self, source: &[u8; 0x20]) -> Result { let mut newkey = *source; let crypter = Aes128::new(GenericArray::from_slice(&self.0)); @@ -107,47 +109,47 @@ impl OptionExt for Option { } } -#[derive(Default, Debug)] +#[derive(Default, Debug, Getters)] pub struct Keys { - secure_boot_key: Option, - tsec_key: Option, - keyblob_keys: [Option; 0x20], - keyblob_mac_keys: [Option; 0x20], - encrypted_keyblobs: [Option; 0x20], - keyblobs: [Option; 0x20], - keyblob_key_sources: [Option; 0x20], - keyblob_mac_key_source: Option, - tsec_root_key: Option, - master_kek_sources: [Option; 0x20], - master_keks: [Option; 0x20], - master_key_source: Option, - master_keys: [Option; 0x20], - package1_keys: [Option; 0x20], - package2_keys: [Option; 0x20], - package2_key_source: Option, - aes_kek_generation_source: Option, - aes_key_generation_source: Option, - key_area_key_application_source: Option, - key_area_key_ocean_source: Option, - key_area_key_system_source: Option, - titlekek_source: Option, - header_kek_source: Option, - sd_card_kek_source: Option, - sd_card_save_key_source: Option, - sd_card_nca_key_source: Option, - save_mac_kek_source: Option, - save_mac_key_source: Option, - header_key_source: Option, - header_key: Option, - titlekeks: [Option; 0x20], - key_area_key_application: [Option; 0x20], - key_area_key_ocean: [Option; 0x20], - key_area_key_system: [Option; 0x20], - sd_card_save_key: Option, - sd_card_nca_key: Option, - nca_hdr_fixed_key_modulus: Option, - acid_fixed_key_modulus: Option, - package2_fixed_key_modulus: Option, + #[get = "pub"] secure_boot_key: Option, + #[get = "pub"] tsec_key: Option, + #[get = "pub"] keyblob_keys: [Option; 0x20], + #[get = "pub"] keyblob_mac_keys: [Option; 0x20], + #[get = "pub"] encrypted_keyblobs: [Option; 0x20], + #[get = "pub"] keyblobs: [Option; 0x20], + #[get = "pub"] keyblob_key_sources: [Option; 0x20], + #[get = "pub"] keyblob_mac_key_source: Option, + #[get = "pub"] tsec_root_key: Option, + #[get = "pub"] master_kek_sources: [Option; 0x20], + #[get = "pub"] master_keks: [Option; 0x20], + #[get = "pub"] master_key_source: Option, + #[get = "pub"] master_keys: [Option; 0x20], + #[get = "pub"] package1_keys: [Option; 0x20], + #[get = "pub"] package2_keys: [Option; 0x20], + #[get = "pub"] package2_key_source: Option, + #[get = "pub"] aes_kek_generation_source: Option, + #[get = "pub"] aes_key_generation_source: Option, + #[get = "pub"] key_area_key_application_source: Option, + #[get = "pub"] key_area_key_ocean_source: Option, + #[get = "pub"] key_area_key_system_source: Option, + #[get = "pub"] titlekek_source: Option, + #[get = "pub"] header_kek_source: Option, + #[get = "pub"] sd_card_kek_source: Option, + #[get = "pub"] sd_card_save_key_source: Option, + #[get = "pub"] sd_card_nca_key_source: Option, + #[get = "pub"] save_mac_kek_source: Option, + #[get = "pub"] save_mac_key_source: Option, + #[get = "pub"] header_key_source: Option, + #[get = "pub"] header_key: Option, + #[get = "pub"] titlekeks: [Option; 0x20], + #[get = "pub"] key_area_key_application: [Option; 0x20], + #[get = "pub"] key_area_key_ocean: [Option; 0x20], + #[get = "pub"] key_area_key_system: [Option; 0x20], + #[get = "pub"] sd_card_save_key: Option, + #[get = "pub"] sd_card_nca_key: Option, + #[get = "pub"] nca_hdr_fixed_key_modulus: Option, + #[get = "pub"] acid_fixed_key_modulus: Option, + #[get = "pub"] package2_fixed_key_modulus: Option, } macro_rules! make_key_macros { diff --git a/src/utils.rs b/src/utils.rs index ba4b1ff..97020d3 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -2,6 +2,84 @@ use num_traits::Num; use core::ops::{Not, BitAnd}; use std::io; +pub struct Hexstring<'a>(pub &'a [u8]); + +impl<'a> core::fmt::Debug for Hexstring<'a> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + for byte in self.0 { + write!(f, "{:02X}", byte)?; + } + Ok(()) + } +} + +#[macro_export] +macro_rules! impl_debug_deserialize_serialize_hexstring { + ($for:ident) => { + impl std::fmt::Debug for $for { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple(stringify!($for)) + .field(&$crate::utils::Hexstring(&self.0[..])) + .finish() + } + } + + impl std::fmt::Display for $for { + fn fmt(&self, f: &mut core::fmt::Formatter) -> std::fmt::Result { + std::fmt::Debug::fmt(&$crate::utils::Hexstring(&self.0[..]), f) + } + } + + impl<'de> serde::Deserialize<'de> for $for { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct StrVisitor; + impl<'de> serde::de::Visitor<'de> for StrVisitor { + type Value = $for; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a character hexstring") + } + + fn visit_str(self, s: &str) -> Result + where + E: serde::de::Error + { + let mut value = [0; std::mem::size_of::<$for>()]; + if s.len() != std::mem::size_of::<$for>() * 2 { + return Err(E::invalid_length(s.len(), &self)) + } + for (idx, c) in s.bytes().enumerate() { + let c = match c { + b'a'..=b'z' => c - b'a' + 10, + b'A'..=b'Z' => c - b'A' + 10, + b'0'..=b'9' => c - b'0', + _ => return Err(E::invalid_value(serde::de::Unexpected::Str(s), &self)) + }; + value[idx / 2] |= c << if idx % 2 == 0 { 4 } else { 0 } + } + + Ok($for(value)) + } + } + + deserializer.deserialize_str(StrVisitor) + } + } + + impl serde::Serialize for $for { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.collect_str(self) + } + } + } +} + pub fn align_down + BitAnd + Copy>(addr: T, align: T) -> T { addr & !(align - T::one()) From 153d7b8ce49f544d385bd8f4784e75a3d0104474 Mon Sep 17 00:00:00 2001 From: roblabla Date: Sat, 22 Dec 2018 18:27:27 +0000 Subject: [PATCH 03/10] Implement Display, Debug and Deserialize on all the PKI types. --- src/pki.rs | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/src/pki.rs b/src/pki.rs index a1d60cc..314313d 100644 --- a/src/pki.rs +++ b/src/pki.rs @@ -12,24 +12,6 @@ use aes::block_cipher_trait::generic_array::GenericArray; use aes::block_cipher_trait::BlockCipher; use getset::Getters; -macro_rules! impl_debug { - ($for:ident) => { - impl fmt::Debug for $for { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - for byte in &self.0[..] { - write!(f, "{:02X}", byte)?; - } - Ok(()) - } - } - } -} - -impl_debug!(Aes128Key); -impl_debug!(AesXtsKey); -impl_debug!(EncryptedKeyblob); -impl_debug!(Keyblob); -impl_debug!(Modulus); #[derive(Clone, Copy)] pub struct Aes128Key([u8; 0x10]); #[derive(Clone, Copy)] @@ -38,6 +20,12 @@ pub struct EncryptedKeyblob([u8; 0xB0]); pub struct Keyblob([u8; 0x90]); pub struct Modulus([u8; 0x100]); +impl_debug_deserialize_serialize_hexstring!(Aes128Key); +impl_debug_deserialize_serialize_hexstring!(AesXtsKey); +impl_debug_deserialize_serialize_hexstring!(EncryptedKeyblob); +impl_debug_deserialize_serialize_hexstring!(Keyblob); +impl_debug_deserialize_serialize_hexstring!(Modulus); + impl EncryptedKeyblob { fn decrypt(&self, key: &Aes128Key) -> Result { let mut keyblob = [0; 0x90]; From c866b95f00e10856727af74031c7d10ccbb49b0b Mon Sep 17 00:00:00 2001 From: roblabla Date: Sat, 22 Dec 2018 18:35:44 +0000 Subject: [PATCH 04/10] PKI: Implement ctr and xts encryption routines. --- Cargo.toml | 3 +++ src/pki.rs | 46 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 629534f..2780a86 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,3 +42,6 @@ derive_more = "0.13" [features] binaries = ["structopt", "cargo_metadata", "scroll", "goblin"] + +[patch.crates-io] +block-modes = { git = 'https://github.com/roblabla/block-ciphers' } diff --git a/src/pki.rs b/src/pki.rs index 314313d..e7b9c1b 100644 --- a/src/pki.rs +++ b/src/pki.rs @@ -6,7 +6,7 @@ use std::io::{self, ErrorKind}; use std::path::Path; use crate::error::Error; use aes::Aes128; -use block_modes::{Ctr128, BlockModeIv, BlockMode}; +use block_modes::{Ctr128, Xts128, BlockModeIv, BlockMode}; use block_modes::block_padding::ZeroPadding; use aes::block_cipher_trait::generic_array::GenericArray; use aes::block_cipher_trait::BlockCipher; @@ -39,6 +39,19 @@ impl EncryptedKeyblob { } impl Aes128Key { + /// Decrypt blocks in CTR mode. + pub fn decrypt_ctr(&self, buf: &mut [u8], ctr: &[u8; 0x10]) -> Result<(), Error> { + if buf.len() % 16 != 0 { + return Err(Error::Crypto(String::from("buf length should be a multiple of 16, the size of an AES block."), Backtrace::new())); + } + + let key = GenericArray::from_slice(&self.0); + let iv = GenericArray::from_slice(ctr); + let mut crypter = Ctr128::::new_fixkey(key, iv); + crypter.decrypt_nopad(buf)?; + Ok(()) + } + pub fn derive_key(&self, source: &[u8; 0x10]) -> Result { let mut newkey = *source; @@ -56,7 +69,38 @@ impl Aes128Key { crypter.decrypt_block(GenericArray::from_mut_slice(&mut newkey[0x10..0x20])); Ok(AesXtsKey(newkey)) + } +} + +fn get_tweak(mut sector: usize) -> [u8; 0x10] { + let mut tweak = [0; 0x10]; + for tweak in tweak.iter_mut().rev() { /* Nintendo LE custom tweak... */ + *tweak = (sector & 0xFF) as u8; + sector >>= 8; + } + tweak +} + +impl AesXtsKey { + pub fn decrypt(&self, src: &[u8], dst: &mut [u8], mut sector: usize, sector_size: usize) -> Result<(), Error> { + if src.len() != dst.len() { + return Err(Error::Crypto(String::from("Src len different from dst len"), Backtrace::new())); + } + if src.len() % sector_size != 0 { + return Err(Error::Crypto(String::from("Length must be multiple of sectors!"), Backtrace::new())); + } + + dst.copy_from_slice(src); + for i in (0..src.len()).step_by(sector_size){ + let tweak = get_tweak(sector); + let key1 = Aes128::new(GenericArray::from_slice(&self.0[0x00..0x10])); + let key2 = Aes128::new(GenericArray::from_slice(&self.0[0x10..0x20])); + let mut crypter = Xts128::::new(key1, key2, GenericArray::from_slice(&tweak)); + crypter.decrypt_nopad(&mut dst[i..i + sector_size])?; + sector += 1; + } + Ok(()) } } From 3a552af43f06acb238def3e1ba9e77e485991dd3 Mon Sep 17 00:00:00 2001 From: roblabla Date: Sat, 22 Dec 2018 18:37:35 +0000 Subject: [PATCH 05/10] NCA: Start parsing. --- Cargo.lock | 15 +- Cargo.toml | 4 +- src/error.rs | 8 + src/format/mod.rs | 1 + src/format/nca.rs | 482 +++++++++++++++++++++++++++++++++++ src/format/nca/structures.rs | 147 +++++++++++ src/lib.rs | 4 + src/utils.rs | 24 ++ 8 files changed, 681 insertions(+), 4 deletions(-) create mode 100644 src/format/nca.rs create mode 100644 src/format/nca/structures.rs diff --git a/Cargo.lock b/Cargo.lock index b4c7303..b9c1771 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -127,10 +127,11 @@ dependencies = [ [[package]] name = "block-modes" version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" +source = "git+https://github.com/roblabla/block-ciphers#b690b504e700b10ea42f9c9c6b5eca658ff2b578" dependencies = [ "block-cipher-trait 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", "block-padding 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -390,7 +391,7 @@ name = "linkle" version = "0.2.6" dependencies = [ "aes 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", - "block-modes 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "block-modes 0.2.0 (git+https://github.com/roblabla/block-ciphers)", "byteorder 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "cargo_metadata 0.6.0 (git+https://github.com/roblabla/cargo_metadata)", "clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -402,12 +403,14 @@ dependencies = [ "goblin 0.0.17 (registry+https://github.com/rust-lang/crates.io-index)", "lz4 1.22.0 (git+https://github.com/bozaro/lz4-rs.git)", "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", + "plain 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "rust-ini 0.13.0 (registry+https://github.com/rust-lang/crates.io-index)", "scroll 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.70 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.70 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)", "sha2 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "static_assertions 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "structopt 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)", "url 1.7.1 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -665,6 +668,11 @@ dependencies = [ "walkdir 2.2.5 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "static_assertions" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "strsim" version = "0.7.0" @@ -859,7 +867,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum blake2-rfc 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)" = "5d6d530bdd2d52966a6d03b7a964add7ae1a288d25214066fd4b600f0f796400" "checksum block-buffer 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a076c298b9ecdb530ed9d967e74a6027d6a7478924520acddcddc24c1c8ab3ab" "checksum block-cipher-trait 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1c924d49bd09e7c06003acda26cd9742e796e34282ec6c1189404dee0c1f4774" -"checksum block-modes 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "283fa06a14026feac8912bf35328fc074f5d68907fd4b9cccad5658a3fc62a30" +"checksum block-modes 0.2.0 (git+https://github.com/roblabla/block-ciphers)" = "" "checksum block-padding 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4fc4358306e344bf9775d0197fd00d2603e5afb0771bb353538630f022068ea3" "checksum byte-tools 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "560c32574a12a89ecd91f5e742165893f86e3ab98d21f8ea548658eb9eef5f40" "checksum byte-tools 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "980479e6fde23246dfb54d47580d66b4e99202e7579c5eaa9fe10ecb5ebd2182" @@ -926,6 +934,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum serde_json 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)" = "c3c6908c7b925cd6c590358a4034de93dbddb20c45e1d021931459fd419bf0e2" "checksum sha2 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9eb6be24e4c23a84d7184280d2722f7f2731fcdd4a9d886efbfe4413e4847ea0" "checksum skeptic 0.13.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c4474d6da9593171bcb086890fc344a3a12783cb24e5b141f8a5d0e43561f4b6" +"checksum static_assertions 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "389ce475f424f267dbed6479cbd8f126c5e1afb053b0acdaa019c74305fc65d1" "checksum strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb4f380125926a99e52bc279241539c018323fab05ad6368b56f93d9369ff550" "checksum structopt 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)" = "41c4a2479a078509940d82773d90ff824a8c89533ab3b59cd3ce8b0c0e369c02" "checksum structopt-derive 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)" = "5352090cfae7a2c85e1a31146268b53396106c88ca5d6ccee2e3fae83b6e35c2" diff --git a/Cargo.toml b/Cargo.toml index 2780a86..e5b8ccb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,8 @@ rust-ini = "0.13" failure = "0.1" dirs = "1.0" getset = "0.0.6" +plain = "0.2" +static_assertions = "0.3" block-modes = "0.2" aes = "0.3" num-traits = "0.2" @@ -44,4 +46,4 @@ derive_more = "0.13" binaries = ["structopt", "cargo_metadata", "scroll", "goblin"] [patch.crates-io] -block-modes = { git = 'https://github.com/roblabla/block-ciphers' } +block-modes = { git = 'https://github.com/roblabla/block-ciphers' } \ No newline at end of file diff --git a/src/error.rs b/src/error.rs index 01de58f..2228ca9 100644 --- a/src/error.rs +++ b/src/error.rs @@ -22,6 +22,14 @@ pub enum Error { RomFsSymlink(PathBuf, Backtrace), #[display(fmt = "Unknown file type at {}", "_0.display()")] RomFsFiletype(PathBuf, Backtrace), + #[display(fmt = "Missing key: {}. Make sure your keyfile is complete.", _0)] + MissingKey(&'static str, Backtrace), + #[display(fmt = "Failed to parse NCA. Make sure your {} key is correct.", _0)] + NcaParse(&'static str, Backtrace), + #[display(fmt = "Missing section {}.", _0)] + MissingSection(usize, Backtrace), + #[display(fmt = "Invalid NCA: {}.", _0)] + InvalidNca(&'static str, Backtrace), } impl Error { diff --git a/src/format/mod.rs b/src/format/mod.rs index b02ca7f..13cd6e1 100644 --- a/src/format/mod.rs +++ b/src/format/mod.rs @@ -2,4 +2,5 @@ pub mod nacp; pub mod nxo; pub mod pfs0; pub mod romfs; +pub mod nca; mod utils; diff --git a/src/format/nca.rs b/src/format/nca.rs new file mode 100644 index 0000000..37e31f3 --- /dev/null +++ b/src/format/nca.rs @@ -0,0 +1,482 @@ +//! NCA Parsing +//! +//! Nintendo Container Archives (NCAs) are signed and encrypted archives that +//! contain software and other content Nintendo provides. Almost every file on +//! the Horizon/NX OS are stored in this container, as it guarantees its +//! authenticity, preventing tempering. +//! +//! For more information about the NCA file format, see the [switchbrew page]. +//! +//! In order to parse an NCA, you may use the `from_file` method: +//! +//! ``` +//! # fn get_nca_file() -> std::io::Result { +//! # std::fs::File::open("tests/fixtures/test.nca") +//! # } +//! let f = get_nca_file()?; +//! let nca = Nca::from_file(nca)?; +//! let section = nca.section(0); +//! ``` +//! +//! [switchbrew page]: https://switchbrew.org/w/index.php?title=NCA_Format + +mod structures; + +use self::structures::*; +use crate::error::Error; +use failure::Backtrace; +use crate::pki::{Keys, Aes128Key, AesXtsKey}; +use std::io::{self, Seek, Read, Write}; +use std::cmp::{min, max}; +use plain::Plain; +use serde_derive::{Deserialize, Serialize}; +use byteorder::{BE, ByteOrder}; +use crate::utils::{align_down, TryClone, ReadRange}; + +#[derive(Debug, Serialize, Deserialize)] +enum KeyType { + Application, Ocean, System +} + +impl From for KeyType { + fn from(from: u8) -> KeyType { + match from { + 0 => KeyType::Application, + 1 => KeyType::Ocean, + 2 => KeyType::System, + unk => panic!("Unknown key type {}", unk) + } + } +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +enum CryptoType { + None, Xts, Ctr, Bktr +} + +impl From for CryptoType { + fn from(from: RawCryptType) -> CryptoType { + match from { + RawCryptType::None => CryptoType::None, + RawCryptType::Xts => CryptoType::Xts, + RawCryptType::Ctr => CryptoType::Ctr, + RawCryptType::Bktr => CryptoType::Bktr, + unk => panic!("Unknown raw crypt type {:?}", unk) + } + } +} + +#[repr(transparent)] +#[derive(Clone, Copy)] +struct Hash([u8; 0x20]); +impl_debug_deserialize_serialize_hexstring!(Hash); + +#[derive(Debug, Serialize, Deserialize, Clone)] +enum FsType { + Pfs0 { + master_hash: Hash, + block_size: u32, + hash_table_offset: u64, + hash_table_size: u64, + pfs0_offset: u64, + pfs0_size: u64, + }, + RomFs, +} + +#[derive(Debug, Serialize, Deserialize)] +enum ContentType { + Program, Meta, Control, Manual, Data, PublicData +} + +impl From for ContentType { + fn from(from: u8) -> ContentType { + match from { + 0 => ContentType::Program, + 1 => ContentType::Meta, + 2 => ContentType::Control, + 3 => ContentType::Manual, + 4 => ContentType::Data, + 5 => ContentType::PublicData, + unk => panic!("Unknown content type {}", unk) + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +enum NcaFormat { + Nca3, Nca2, Nca0 +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct SectionJson { + media_start_offset: u32, + media_end_offset: u32, + crypto: CryptoType, + fstype: FsType, + nounce: u64, +} + +#[derive(Serialize, Deserialize)] +#[repr(transparent)] +pub struct TitleId(u64); + +impl std::fmt::Debug for TitleId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:016x}", self.0) + } +} + +#[derive(Debug, Serialize, Deserialize)] +struct NcaJson { + format: NcaFormat, + sig: structures::SigDebug, + npdm_sig: structures::SigDebug, + is_gamecard: bool, + content_type: ContentType, + key_revision1: u8, + key_revision2: u8, + key_type: KeyType, + title_id: TitleId, + sdk_version: u32, // TODO: Better format + xts_key: AesXtsKey, + ctr_key: Aes128Key, + rights_id: Option<[u8; 0x10]>, + sections: [Option; 4] +} + +#[derive(Debug)] +pub struct Nca { + stream: R, + json: NcaJson +} + +fn get_key_area_key(pki: &Keys, key_version: usize, key_type: KeyType) -> Result { + let key = match key_type { + KeyType::Application => pki.key_area_key_application()[key_version], + KeyType::Ocean => pki.key_area_key_ocean()[key_version], + KeyType::System => pki.key_area_key_system()[key_version], + }; + key.ok_or(Error::MissingKey(Box::leak(format!("key_area_key_application_{:02x}", key_version).into_boxed_str()), Backtrace::new())) +} + +fn decrypt_header(pki: &Keys, file: &mut Read) -> Result { + // Decrypt header. + let mut header = [0; 0xC00]; + let mut decrypted_header = [0; 0xC00]; + + file.read_exact(&mut header)?; + + // TODO: Check if NCA is already decrypted + + let header_key = pki.header_key().as_ref().ok_or(Error::MissingKey("header_key", Backtrace::new()))?; + header_key.decrypt(&header[..0x400], &mut decrypted_header[..0x400], 0, 0x200)?; + + let raw_nca = *RawNca::from_bytes(&decrypted_header).expect("RawNca to be of the right size"); + match &raw_nca.magic { + b"NCA3" => { + header_key.decrypt(&header, &mut decrypted_header, 0, 0x200)?; + }, + b"NCA2" => { + for (i, fsheader) in raw_nca.fs_headers.iter().enumerate() { + let offset = 0x400 + i * 0x200; + if fsheader._0x148[0] != 0 || fsheader._0x148[..0xB7] != fsheader._0x148[1..] { + header_key.decrypt(&header[offset..offset + 0x200], &mut decrypted_header[offset..offset + 0x200], 0, 0x200)?; + } else { + decrypted_header[offset..offset + 0x200].copy_from_slice(&[0; 0x200]); + } + } + }, + b"NCA0" => unimplemented!("NCA0 parsing is not implemented yet"), + _ => return Err(Error::NcaParse("header_key", Backtrace::new())) + } + Ok(*RawNca::from_bytes(&decrypted_header).expect("RawNca to be of the right size")) +} + +impl Nca { + pub fn from_file(pki: &Keys, mut file: R) -> Result, Error> { + let header = decrypt_header(pki, &mut file)?; + let format = match &header.magic { + b"NCA3" => NcaFormat::Nca3, + b"NCA2" => NcaFormat::Nca2, + b"NCA0" => NcaFormat::Nca0, + _ => unreachable!() + }; + + // TODO: NCA: Verify header with RSA2048 PSS + // BODY: We want to make sure the NCAs have a valid signature before + // BODY: decrypting. Maybe put it behind a flag that accepts invalidly + // BODY: signed NCAs? + + // Crypto is stupid. First, we need to get the max of crypto_type and crypto_type2. + // Then, nintendo uses both 0 and 1 as master key 0, and then everything is shifted by one. + // So we sub by 1. + let master_key_revision = max(header.crypto_type2, header.crypto_type).saturating_sub(1); + + // Handle Rights ID. + let has_rights_id = header.rights_id != [0; 0x10]; + + let key_area_key = get_key_area_key(pki, master_key_revision as _, KeyType::from(header.key_index))?; + + let decrypted_keys = if !has_rights_id { + // TODO: NCA0 => return + ( + key_area_key.derive_xts_key(&header.encrypted_xts_key)?, + key_area_key.derive_key(&header.encrypted_ctr_key)?, + ) + } else { + // TODO: Implement RightsID crypto. + unimplemented!("Rights ID"); + }; + + // Parse sections + let mut sections = [None, None, None, None]; + for (idx, (section, fs)) in header.section_entries.iter().zip(header.fs_headers.iter()).enumerate() { + // Check if section is present + if section.media_start_offset != 0 { + if has_rights_id { + unimplemented!("Rights ID"); + } else { + assert_eq!(fs.version, 2, "Invalid NCA FS Header version"); + unsafe { + sections[idx] = Some(SectionJson { + crypto: fs.crypt_type.into(), + fstype: match fs.fs_type { + RawFsType::Pfs0 => FsType::Pfs0 { + master_hash: Hash(fs.superblock.pfs0.master_hash), + block_size: fs.superblock.pfs0.block_size, + hash_table_offset: fs.superblock.pfs0.hash_table_offset, + hash_table_size: fs.superblock.pfs0.hash_table_size, + pfs0_offset: fs.superblock.pfs0.pfs0_offset, + pfs0_size: fs.superblock.pfs0.pfs0_size, + }, + RawFsType::RomFs => FsType::RomFs, + _ => unreachable!() + }, + nounce: fs.section_ctr, + media_start_offset: section.media_start_offset, + media_end_offset: section.media_end_offset, + }); + } + } + } + } + + let nca = Nca { + stream: file, + json: NcaJson { + format, + sig: header.fixed_key_sig, + npdm_sig: header.npdm_sig, + is_gamecard: header.is_gamecard != 0, + content_type: ContentType::from(header.content_type), + key_revision1: header.crypto_type, + key_revision2: header.crypto_type2, + key_type: KeyType::from(header.key_index), + title_id: TitleId(header.titleid), + // TODO: Store the SDK version in a more human readable format. + sdk_version: header.sdk_version, + xts_key: decrypted_keys.0, + ctr_key: decrypted_keys.1, + // TODO: Implement rights id. + rights_id: None, + sections: sections, + } + }; + + Ok(nca) + } +} + +impl Nca { + pub fn raw_section(&self, id: usize) -> Result>, Error> { + if let Some(section) = &self.json.sections[id] { + // TODO: Nca::raw_section should reopen the file, not dup2 the handle. + let mut stream = self.stream.try_clone()?; + stream.seek(std::io::SeekFrom::Start(section.start_offset()))?; + + Ok(CryptoStream { + stream: ReadRange::new(stream, section.start_offset(), section.size()), + // Keep a 1-block large buffer of data in case of partial reads. + buffer: [0; 0x10], + state: CryptoStreamState { + ctr_key: self.json.ctr_key, + xts_key: self.json.xts_key, + json: section.clone(), + offset: section.start_offset(), + } + }) + } else { + return Err(Error::MissingSection(id, Backtrace::new())) + } + } + + pub fn section(&self, id: usize) -> Result>>, Error> { + let mut raw_section = self.raw_section(id)?; + let start_offset = match raw_section.state.json.fstype { + FsType::Pfs0 { pfs0_offset, .. } => pfs0_offset, + _ => 0 + }; + raw_section.seek(io::SeekFrom::Start(start_offset))?; + Ok(VerificationStream { + stream: raw_section, + start_at: start_offset, + }) + } +} + +impl Nca { + pub fn write_header(&self, file: &mut Write) -> Result<(), Error> { + unimplemented!() + } + + pub fn write_json(&self, file: &mut Write) -> Result<(), Error> { + serde_json::to_writer_pretty(file, &self.json).unwrap(); + Ok(()) + } +} + +impl SectionJson { + fn start_offset(&self) -> u64 { + self.media_start_offset as u64 * 0x200 + } + fn size(&self) -> u64 { + (self.media_end_offset - self.media_start_offset) as u64 * 0x200 + } +} + +/// A wrapper around a Read/Seek stream, decrypting its contents based of an +/// NCA Section. +#[derive(Debug)] +pub struct CryptoStream { + stream: R, + // Hello borrowck my old friend. We need to keep the state separate from the + // buffer, otherwise we get borrow problems. + state: CryptoStreamState, + // Keep a 1-block large buffer of data in case of partial reads. + buffer: [u8; 0x10], +} + +#[derive(Debug)] +struct CryptoStreamState { + ctr_key: Aes128Key, + xts_key: AesXtsKey, + offset: u64, + json: SectionJson, +} + +impl CryptoStreamState { + fn get_ctr(&self) -> [u8; 0x10] { + println!("{:x} {:x}", self.json.start_offset(), self.offset); + let offset = (self.json.start_offset() + self.offset) / 16; + let mut ctr = [0; 0x10]; + // Write section nounce in Big Endian. + BE::write_u64(&mut ctr[..8], self.json.nounce); + // Set ctr to offset / BLOCK_SIZE, in big endian. + BE::write_u64(&mut ctr[8..], offset); + ctr + } + + fn decrypt(&mut self, buf: &mut [u8]) { + match self.json.crypto { + CryptoType::Ctr => { + self.ctr_key.decrypt_ctr(buf, &self.get_ctr()); + }, + CryptoType::Xts => { + unimplemented!("XTS crypto") + }, + CryptoType::Bktr => { + unimplemented!("Bktr crypto") + } + CryptoType::None => () + } + } +} + +/// Read implementation for CryptoStream. +impl Read for CryptoStream { + fn read(&mut self, mut buf: &mut [u8]) -> io::Result { + let previous_leftovers = (self.state.offset % 16) as usize; + if previous_leftovers != 0 { + // First, handle leftovers from a previous read call, so we go back + // to a properly block-aligned read. + let to = min(previous_leftovers + buf.len(), 16); + let size = to - previous_leftovers; + buf[..size].copy_from_slice(&self.buffer[previous_leftovers..to]); + self.state.offset += size as u64; + + buf = &mut buf[size..]; + } + + let read = self.stream.read(buf)?; + buf = &mut buf[..read]; + + // Decrypt all the non-leftover bytes. + let len_no_leftovers = align_down(buf.len(), 16); + self.state.decrypt(&mut buf[..len_no_leftovers]); + self.state.offset += len_no_leftovers as u64; + let leftovers = buf.len() % 16; + if leftovers != 0 { + // We got some leftover, save them in the internal buffer, finish + // reading it, decrypt it, and copy the part we want back. + // + // Why not delay decryption until we have a full block? Well, that's + // because the read interface is **stupid**. If we ever return 0, + // the file is assumed to be finished - instead of signaling "herp, + // needs more bytes". So we play greedy. + let from = align_down(buf.len(), 16); + self.buffer[..leftovers].copy_from_slice(&buf[from..buf.len()]); + self.stream.read_exact(&mut self.buffer[leftovers..])?; + self.state.decrypt(&mut self.buffer); + buf[from..].copy_from_slice(&self.buffer[..leftovers]); + self.state.offset += leftovers as u64; + } + + Ok(buf.len() + if previous_leftovers != 0 { (16 - previous_leftovers) } else { 0 }) + } +} + +impl Seek for CryptoStream { + fn seek(&mut self, from: io::SeekFrom) -> io::Result { + self.state.offset = match from { + io::SeekFrom::Start(cur) => cur, + io::SeekFrom::Current(val) => (self.state.offset as i64 + val) as u64, + io::SeekFrom::End(val) => (self.state.json.size() as i64 + val) as u64, + }; + + let aligned_offset = align_down(self.state.offset, 16); + self.stream.seek(io::SeekFrom::Start(aligned_offset)); + if self.state.offset % 16 != 0 { + self.stream.read_exact(&mut self.buffer)?; + self.state.decrypt(&mut self.buffer); + } + Ok(self.state.offset) + } +} + +// TODO: Make VerificationStream actually verify the data it reads. +// BODY: VerificationStream should verify the body based on the hash table +// BODY: located with the help of the superblock. It will need a different +// BODY: implementation for PFS0 and RomFs (and maybe Bktr?). +pub struct VerificationStream { + stream: R, + start_at: u64, +} + +impl Read for VerificationStream { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + self.stream.read(buf) + } +} +/* +impl Seek for VerificationStream { + fn seek(&mut self, from: io::SeekFrom) -> io::Result { + let from = match from { + io::SeekFrom::Start(cur) => io::SeekFrom::Start(self.start_at + cur), + io::SeekFrom::Current(val) => io::SeekFrom::Start(self.pos + val), + io::SeekFrom::End(val) => io::SeekFrom::Start(val), + } + self.stream.seek(self.start_at + from) + } +} +*/ diff --git a/src/format/nca/structures.rs b/src/format/nca/structures.rs new file mode 100644 index 0000000..769f0cb --- /dev/null +++ b/src/format/nca/structures.rs @@ -0,0 +1,147 @@ +//! Raw NCA structures +//! +//! Those are used by the NCA parsing code, basically casting the byte slices +//! to those types through the plain crate. This only works on Little Endian +//! hosts! Ideally, we would have a derive macro that generates an +//! appropriate parser based on the machine's endianness. + +use std::fmt; +use plain::Plain; + +#[repr(transparent)] +#[derive(Clone, Copy)] +pub struct SigDebug([u8; 0x100]); +#[derive(Clone, Copy)] +pub struct SkipDebug(T); + +impl fmt::Debug for SkipDebug { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "SkipDebug")?; + Ok(()) + } +} + +impl_debug_deserialize_serialize_hexstring!(SigDebug); + +#[derive(Debug, Clone, Copy)] +#[repr(C)] +pub struct RawNca { + pub fixed_key_sig: SigDebug, + pub npdm_sig: SigDebug, + pub magic: [u8; 4], + pub is_gamecard: u8, + pub content_type: u8, + pub crypto_type: u8, + pub key_index: u8, + pub nca_size: u64, + pub titleid: u64, + pub _padding0: SkipDebug, + pub sdk_version: u32, + pub crypto_type2: u8, + pub _padding1: SkipDebug<[u8; 0xF]>, + pub rights_id: [u8; 0x10], + pub section_entries: [RawSectionTableEntry; 4], + pub section_hashes: [[u8; 0x20]; 4], + pub encrypted_xts_key: [u8; 0x20], + pub encrypted_ctr_key: [u8; 0x10], + pub unknown_new_key: [u8; 0x10], + pub _padding2: SkipDebug<[u8; 0xC0]>, + pub fs_headers: [RawNcaFsHeader; 4] +} +assert_eq_size!(assert_nca_size; RawNca, [u8; 0xC00]); + +#[derive(Debug, Clone, Copy)] +#[repr(C)] +pub struct RawPfs0Superblock { + pub master_hash: [u8; 0x20], + pub block_size: u32, + pub always_2: u32, + pub hash_table_offset: u64, + pub hash_table_size: u64, + pub pfs0_offset: u64, + pub pfs0_size: u64, + pub _0x48: SkipDebug<[u8; 0xF0]>, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union RawSuperblock { + pub pfs0: RawPfs0Superblock, + //romfs_superblock: RomfsSuperblock, + //bktrs_superblock: BktrSuperblock, + //nca0_romfs_superblock: Nca0RomfsSuperblock, +} +assert_eq_size!(assert_superblock_size; RawSuperblock, [u8; 0x138]); + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct RawNcaFsHeader { + pub version: u16, + pub partition_type: RawPartitionType, + pub fs_type: RawFsType, + pub crypt_type: RawCryptType, + pub _0x5: [u8; 0x3], + pub superblock: RawSuperblock, + pub section_ctr: u64, + pub _0x148: [u8; 0xB8] +} +assert_eq_size!(assert_nca_fs_header_size; RawNcaFsHeader, [u8; 0x148 + 0xB8]); + +enum_with_val! { + #[derive(Clone, Copy)] + pub struct RawPartitionType(u8) { + RomFs = 0, Pfs0 = 1 + } +} + +enum_with_val! { + #[derive(Clone, Copy)] + pub struct RawFsType(u8) { + Pfs0 = 2, RomFs = 3 + } +} + +enum_with_val! { + #[derive(Clone, Copy)] + pub struct RawCryptType(u8) { + None = 1, Xts = 2, Ctr = 3, Bktr = 4 + } +} + +pub struct RawSuperblockWithTag(RawPartitionType, RawCryptType, RawSuperblock); +impl fmt::Debug for RawSuperblockWithTag { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match (self.0, self.1) { + (RawPartitionType::RomFs, RawCryptType::Bktr) => f.debug_struct("RawSuperblock::Bktr").finish(), + (RawPartitionType::RomFs, _) => f.debug_struct("RawSuperblock::RomFs").finish(), + (RawPartitionType::Pfs0, _) => unsafe { f.debug_struct("RawSuperblock::Pfs0").field("inner", &self.2.pfs0).finish() }, + _ => unreachable!() + } + } +} + +impl fmt::Debug for RawNcaFsHeader { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("RawNcaFsHeader") + .field("version", &self.version) + .field("partition_type", &self.partition_type) + .field("fs_type", &self.fs_type) + .field("crypt_type", &self.crypt_type) + .field("_0x5", &self._0x5) + .field("superblock", &RawSuperblockWithTag(self.partition_type, self.crypt_type, self.superblock)) + .field("section_ctr", &self.section_ctr) + .finish() + } +} + +// Safety: RawNca is a repr(C) that only contains Pods itself. +unsafe impl Plain for RawNca {} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct RawSectionTableEntry { + pub media_start_offset: u32, + pub media_end_offset: u32, + pub unknown1: u32, + pub unknown2: u32 +} diff --git a/src/lib.rs b/src/lib.rs index dfc8c68..17f2d1a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,7 @@ +// TODO: Figure out why this is necessary. +#[macro_use] +extern crate static_assertions; + #[macro_use] mod utils; pub mod format; diff --git a/src/utils.rs b/src/utils.rs index 97020d3..41e8971 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -2,6 +2,30 @@ use num_traits::Num; use core::ops::{Not, BitAnd}; use std::io; +// TODO: Document +#[macro_export] +macro_rules! enum_with_val { + ($(#[$meta:meta])* $vis:vis struct $ident:ident($ty:ty) { + $($variant:ident = $num:expr),* $(,)* + }) => { + $(#[$meta])* + #[derive(PartialEq, Eq)] + $vis struct $ident($ty); + impl $ident { + $(#[allow(non_upper_case_globals)] $vis const $variant: $ident = $ident($num);)* + } + + impl ::core::fmt::Debug for $ident { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + match self { + $(&$ident::$variant => f.write_str(stringify!($ident)),)* + &$ident(v) => write!(f, "UNKNOWN({})", v), + } + } + } + } +} + pub struct Hexstring<'a>(pub &'a [u8]); impl<'a> core::fmt::Debug for Hexstring<'a> { From dfdd6d67924b47a38d2dbebe2096e05ec011193c Mon Sep 17 00:00:00 2001 From: roblabla Date: Sat, 22 Dec 2018 18:39:27 +0000 Subject: [PATCH 06/10] CLAP: Add a simple extract command. --- src/bin/linkle_clap.rs | 78 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 74 insertions(+), 4 deletions(-) diff --git a/src/bin/linkle_clap.rs b/src/bin/linkle_clap.rs index 2373f43..2b29ddf 100644 --- a/src/bin/linkle_clap.rs +++ b/src/bin/linkle_clap.rs @@ -2,7 +2,7 @@ extern crate structopt; extern crate linkle; -use std::fs::{OpenOptions}; +use std::fs::{OpenOptions, File}; use std::path::{Path, PathBuf}; use std::process; use structopt::StructOpt; @@ -66,6 +66,37 @@ enum Opt { #[structopt(parse(from_os_str))] output_file: PathBuf, }, + /// Extract an NCA file. + #[structopt(name = "nca_extract")] + NcaExtract { + /// Sets the input file to use. + #[structopt(parse(from_os_str))] + input_file: PathBuf, + + /// Sets the output file to extract the header to. + #[structopt(parse(from_os_str), long = "header-json")] + header_file: Option, + + /// Sets the output file to extract the section0 to. + #[structopt(parse(from_os_str), long = "section0")] + section0_file: Option, + + /// Sets the output file to extract the section1 to. + #[structopt(parse(from_os_str), long = "section1")] + section1_file: Option, + + /// Sets the output file to extract the section2 to. + #[structopt(parse(from_os_str), long = "section2")] + section2_file: Option, + + /// Sets the output file to extract the section3 to. + #[structopt(parse(from_os_str), long = "section3")] + section3_file: Option, + + /// Use development keys instead of retail + #[structopt(short = "d", long = "dev")] + dev: bool + } } fn create_nxo(format: &str, input_file: &str, output_file: &str, icon_file: Option<&str>, romfs_dir: Option<&str>, nacp_file: Option<&str>) -> Result<(), linkle::error::Error> { @@ -124,17 +155,56 @@ fn create_romfs(input_directory: &Path, output_file: &Path) -> Result<(), linkle Ok(()) } -fn to_opt_str(s: &Option) -> Option<&str> { - s.as_ref().map(String::as_ref) +fn to_opt_ref>(s: &Option) -> Option<&U> { + s.as_ref().map(AsRef::as_ref) +} + +fn extract_nca(input_file: &Path, is_dev: bool, output_header_json: Option<&Path>, + output_section0: Option<&Path>, output_section1: Option<&Path>, + output_section2: Option<&Path>, output_section3: Option<&Path>) -> std::io::Result<()> { + let keys = if is_dev { + linkle::pki::Keys::new_dev(None).unwrap() + } else { + linkle::pki::Keys::new_retail(None).unwrap() + }; + let nca = linkle::format::nca::Nca::from_file(&keys, File::open(input_file)?).unwrap(); + if let Some(output_header_json) = output_header_json { + let mut output_header_json = File::create(output_header_json)?; + nca.write_json(&mut output_header_json).unwrap(); + } + if let Some(output_section0) = output_section0 { + let mut output_section0 = File::create(output_section0)?; + let mut section = nca.section(0).unwrap(); + std::io::copy(&mut section, &mut output_section0)?; + } + if let Some(output_section1) = output_section1 { + let mut output_section1 = File::create(output_section1)?; + let mut section = nca.section(1).unwrap(); + std::io::copy(&mut section, &mut output_section1)?; + } + if let Some(output_section2) = output_section2 { + let mut output_section2 = File::create(output_section2)?; + let mut section = nca.section(2).unwrap(); + std::io::copy(&mut section, &mut output_section2)?; + } + if let Some(output_section3) = output_section3 { + let mut output_section3 = File::create(output_section3)?; + let mut section = nca.section(3).unwrap(); + std::io::copy(&mut section, &mut output_section3)?; + } + Ok(()) } fn process_args(app: &Opt) { let res = match app { - Opt::Nro { ref input_file, ref output_file, ref icon, ref romfs, ref nacp } => create_nxo("nro", input_file, output_file, to_opt_str(icon), to_opt_str(romfs), to_opt_str(nacp)), + Opt::Nro { ref input_file, ref output_file, ref icon, ref romfs, ref nacp } => create_nxo("nro", input_file, output_file, to_opt_ref(icon), to_opt_ref(romfs), to_opt_ref(nacp)), Opt::Nso { ref input_file, ref output_file } => create_nxo("nro", input_file, output_file, None, None, None), Opt::Pfs0 { ref input_directory, ref output_file } => create_pfs0(input_directory, output_file), Opt::Nacp { ref input_file, ref output_file } => create_nacp(input_file, output_file), Opt::Romfs { ref input_directory, ref output_file } => create_romfs(input_directory, output_file), + Opt::NcaExtract { ref input_file, ref header_file, ref section0_file, + ref section1_file, ref section2_file, + ref section3_file, dev } => extract_nca(input_file, *dev, to_opt_ref(header_file), to_opt_ref(section0_file), to_opt_ref(section1_file), to_opt_ref(section2_file), to_opt_ref(section3_file)), }; if let Err(e) = res { From 935fed26b7da56583d53b44fa4f1c2acfd125217 Mon Sep 17 00:00:00 2001 From: roblabla Date: Sat, 22 Dec 2018 18:39:52 +0000 Subject: [PATCH 07/10] Run clippy on CI. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 1c04571..703655d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -48,7 +48,7 @@ matrix: # - env: TARGET=powerpc64le-unknown-linux-gnu # - env: TARGET=s390x-unknown-linux-gnu DISABLE_TESTS=1 # - env: TARGET=x86_64-unknown-linux-gnu - - env: TARGET=x86_64-unknown-linux-musl + - env: TARGET=x86_64-unknown-linux-musl ENABLE_CLIPPY=1 # OSX - env: TARGET=i686-apple-darwin From 9ba46f8aca8299c7c7e1e44fa5918b1d6792ae33 Mon Sep 17 00:00:00 2001 From: roblabla Date: Thu, 24 Jan 2019 02:18:51 +0000 Subject: [PATCH 08/10] Add CMAC verification of keyblobs, generate encrypted keyblobs --- Cargo.lock | 37 +++++++++++++++++++++++++++++++++++++ Cargo.toml | 3 ++- src/bin/linkle_clap.rs | 22 ++++++++++++++-------- src/error.rs | 8 ++++++++ src/pki.rs | 33 ++++++++++++++++++++++++++++----- 5 files changed, 89 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b9c1771..6d99ade 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -215,11 +215,38 @@ dependencies = [ "vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "cmac" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "block-cipher-trait 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", + "crypto-mac 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "dbl 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "constant_time_eq" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "crypto-mac" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "generic-array 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)", + "subtle 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "dbl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "generic-array 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "derive_more" version = "0.13.0" @@ -395,6 +422,7 @@ dependencies = [ "byteorder 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "cargo_metadata 0.6.0 (git+https://github.com/roblabla/cargo_metadata)", "clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "cmac 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "derive_more 0.13.0 (registry+https://github.com/rust-lang/crates.io-index)", "dirs 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", "elf 0.0.10 (registry+https://github.com/rust-lang/crates.io-index)", @@ -698,6 +726,11 @@ dependencies = [ "syn 0.15.22 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "subtle" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "syn" version = "0.13.11" @@ -879,7 +912,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum cc 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)" = "70f2a88c2e69ceee91c209d8ef25b81fc1a65f42c7f14dfd59d1fed189e514d1" "checksum cfg-if 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "0c4e7bb64a8ebb0d856483e1e682ea3422f883c5f5615a90d51a2c82fe87fdd3" "checksum clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b957d88f4b6a63b9d70d5f454ac8011819c6efa7727858f458ab71c756ce2d3e" +"checksum cmac 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6f4a435124bcc292eba031f1f725d7abacdaf13cbf9f935450e8c45aa9e96cad" "checksum constant_time_eq 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "8ff012e225ce166d4422e0e78419d901719760f62ae2b7969ca6b564d1b54a9e" +"checksum crypto-mac 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4434400df11d95d556bac068ddfedd482915eb18fe8bea89bc80b6e4b1c179e5" +"checksum dbl 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6c40b13b561e11560d7b12785e74113a3163df617e2fbce60ce1764e0b270eaa" "checksum derive_more 0.13.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3f57d78cf3bd45270dad4e70c21ec77a960b36c7a841ff9db76aaa775a8fb871" "checksum digest 0.7.5 (registry+https://github.com/rust-lang/crates.io-index)" = "5b29c278aa8fd30796bd977169e8004b4aa88cdcd2f32a6eb22bc2d5d38df94a" "checksum dirs 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "88972de891f6118092b643d85a0b28e0678e0f948d7f879aa32f2d5aafe97d2a" @@ -938,6 +974,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb4f380125926a99e52bc279241539c018323fab05ad6368b56f93d9369ff550" "checksum structopt 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)" = "41c4a2479a078509940d82773d90ff824a8c89533ab3b59cd3ce8b0c0e369c02" "checksum structopt-derive 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)" = "5352090cfae7a2c85e1a31146268b53396106c88ca5d6ccee2e3fae83b6e35c2" +"checksum subtle 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2d67a5a62ba6e01cb2192ff309324cb4875d0c451d55fe2319433abe7a05a8ee" "checksum syn 0.13.11 (registry+https://github.com/rust-lang/crates.io-index)" = "14f9bf6292f3a61d2c716723fdb789a41bbe104168e6f496dc6497e531ea1b9b" "checksum syn 0.14.7 (registry+https://github.com/rust-lang/crates.io-index)" = "e2e13df71f29f9440b50261a5882c86eac334f1badb3134ec26f0de2f1418e44" "checksum syn 0.15.22 (registry+https://github.com/rust-lang/crates.io-index)" = "ae8b29eb5210bc5cf63ed6149cbf9adfc82ac0be023d8735c176ee74a2db4da7" diff --git a/Cargo.toml b/Cargo.toml index e5b8ccb..9b17cf0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,9 +41,10 @@ block-modes = "0.2" aes = "0.3" num-traits = "0.2" derive_more = "0.13" +cmac = "0.2.0" [features] binaries = ["structopt", "cargo_metadata", "scroll", "goblin"] [patch.crates-io] -block-modes = { git = 'https://github.com/roblabla/block-ciphers' } \ No newline at end of file +block-modes = { git = 'https://github.com/roblabla/block-ciphers' } diff --git a/src/bin/linkle_clap.rs b/src/bin/linkle_clap.rs index 2b29ddf..41bec97 100644 --- a/src/bin/linkle_clap.rs +++ b/src/bin/linkle_clap.rs @@ -95,7 +95,11 @@ enum Opt { /// Use development keys instead of retail #[structopt(short = "d", long = "dev")] - dev: bool + dev: bool, + + /// Keyfile + #[structopt(parse(from_os_str), short = "k", long = "keyset")] + keyfile: Option, } } @@ -159,15 +163,17 @@ fn to_opt_ref>(s: &Option) -> Option<&U> { s.as_ref().map(AsRef::as_ref) } -fn extract_nca(input_file: &Path, is_dev: bool, output_header_json: Option<&Path>, +fn extract_nca(input_file: &Path, is_dev: bool, key_path: Option<&Path>, + output_header_json: Option<&Path>, output_section0: Option<&Path>, output_section1: Option<&Path>, - output_section2: Option<&Path>, output_section3: Option<&Path>) -> std::io::Result<()> { + output_section2: Option<&Path>, output_section3: Option<&Path>) -> Result<(), linkle::error::Error> { let keys = if is_dev { - linkle::pki::Keys::new_dev(None).unwrap() + linkle::pki::Keys::new_dev(key_path).unwrap() } else { - linkle::pki::Keys::new_retail(None).unwrap() + linkle::pki::Keys::new_retail(key_path).unwrap() }; - let nca = linkle::format::nca::Nca::from_file(&keys, File::open(input_file)?).unwrap(); + println!("{:#?}", keys); + /*let nca = linkle::format::nca::Nca::from_file(&keys, File::open(input_file)?).unwrap(); if let Some(output_header_json) = output_header_json { let mut output_header_json = File::create(output_header_json)?; nca.write_json(&mut output_header_json).unwrap(); @@ -191,7 +197,7 @@ fn extract_nca(input_file: &Path, is_dev: bool, output_header_json: Option<&Path let mut output_section3 = File::create(output_section3)?; let mut section = nca.section(3).unwrap(); std::io::copy(&mut section, &mut output_section3)?; - } + }*/ Ok(()) } @@ -204,7 +210,7 @@ fn process_args(app: &Opt) { Opt::Romfs { ref input_directory, ref output_file } => create_romfs(input_directory, output_file), Opt::NcaExtract { ref input_file, ref header_file, ref section0_file, ref section1_file, ref section2_file, - ref section3_file, dev } => extract_nca(input_file, *dev, to_opt_ref(header_file), to_opt_ref(section0_file), to_opt_ref(section1_file), to_opt_ref(section2_file), to_opt_ref(section3_file)), + ref section3_file, dev, ref keyfile } => extract_nca(input_file, *dev, to_opt_ref(keyfile), to_opt_ref(header_file), to_opt_ref(section0_file), to_opt_ref(section1_file), to_opt_ref(section2_file), to_opt_ref(section3_file)), }; if let Err(e) = res { diff --git a/src/error.rs b/src/error.rs index 2228ca9..eefb41f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -30,6 +30,8 @@ pub enum Error { MissingSection(usize, Backtrace), #[display(fmt = "Invalid NCA: {}.", _0)] InvalidNca(&'static str, Backtrace), + #[display(fmt = "Invalid keyblob {}: {}.", _1, _0)] + MacError(cmac::crypto_mac::MacError, usize, Backtrace), } impl Error { @@ -75,3 +77,9 @@ impl From for Error { Error::BlockMode(err, Backtrace::new()) } } + +impl From<(usize, cmac::crypto_mac::MacError)> for Error { + fn from((id, err): (usize, cmac::crypto_mac::MacError)) -> Error { + Error::MacError(err, id, Backtrace::new()) + } +} diff --git a/src/pki.rs b/src/pki.rs index e7b9c1b..2c7f9d3 100644 --- a/src/pki.rs +++ b/src/pki.rs @@ -6,11 +6,13 @@ use std::io::{self, ErrorKind}; use std::path::Path; use crate::error::Error; use aes::Aes128; +use cmac::Cmac; use block_modes::{Ctr128, Xts128, BlockModeIv, BlockMode}; use block_modes::block_padding::ZeroPadding; use aes::block_cipher_trait::generic_array::GenericArray; use aes::block_cipher_trait::BlockCipher; use getset::Getters; +use cmac::crypto_mac::Mac; #[derive(Clone, Copy)] pub struct Aes128Key([u8; 0x10]); @@ -26,11 +28,30 @@ impl_debug_deserialize_serialize_hexstring!(EncryptedKeyblob); impl_debug_deserialize_serialize_hexstring!(Keyblob); impl_debug_deserialize_serialize_hexstring!(Modulus); +impl Keyblob { + fn encrypt(&self, key: &Aes128Key, mac_key: &Aes128Key, keyblob_id: usize) -> Result { + let mut encrypted_keyblob = [0; 0xB0]; + encrypted_keyblob[0x20..].copy_from_slice(&self.0); + + let mut crypter = Ctr128::::new_fixkey(GenericArray::from_slice(&key.0), GenericArray::from_slice(&encrypted_keyblob[0x10..0x20])); + crypter.encrypt_nopad(&mut encrypted_keyblob[0x20..])?; + + let mut cmac = Cmac::::new_varkey(&mac_key.0[..]).unwrap(); + cmac.input(&encrypted_keyblob[0x10..]); + encrypted_keyblob[..0x10].copy_from_slice(cmac.result().code().as_slice()); + Ok(EncryptedKeyblob(encrypted_keyblob)) + } +} + impl EncryptedKeyblob { - fn decrypt(&self, key: &Aes128Key) -> Result { + fn decrypt(&self, key: &Aes128Key, mac_key: &Aes128Key, keyblob_id: usize) -> Result { let mut keyblob = [0; 0x90]; keyblob.copy_from_slice(&self.0[0x20..]); + let mut cmac = Cmac::::new_varkey(&mac_key.0[..]).unwrap(); + cmac.input(&self.0[0x10..]); + cmac.verify(&self.0[..0x10]).map_err(|err| (keyblob_id, err))?; + let mut crypter = Ctr128::::new_fixkey(GenericArray::from_slice(&key.0), GenericArray::from_slice(&self.0[0x10..0x20])); crypter.decrypt_nopad(&mut keyblob)?; @@ -473,10 +494,12 @@ impl Keys { } } for i in 0..6 { - match (&self.keyblob_keys[i], &self.keyblob_mac_keys[i], &self.encrypted_keyblobs[i]) { - (Some(keyblob_key), Some(_keyblob_mac_key), Some(encrypted_keyblob)) => { - // TODO: Calculate cmac - self.keyblobs[i] = Some(encrypted_keyblob.decrypt(keyblob_key)?); + match (&self.keyblob_keys[i], &self.keyblob_mac_keys[i], &mut self.encrypted_keyblobs[i], &mut self.keyblobs[i]) { + (Some(keyblob_key), Some(keyblob_mac_key), Some(encrypted_keyblob), ref mut keyblob @ None) => { + **keyblob = Some(encrypted_keyblob.decrypt(keyblob_key, keyblob_mac_key, i)?); + }, + (Some(keyblob_key), Some(keyblob_mac_key), ref mut encrypted_keyblob @ None, Some(keyblob)) => { + **encrypted_keyblob = Some(keyblob.encrypt(keyblob_key, keyblob_mac_key, i)?); }, _ => continue } From 792aea478bda16fdc092f26128e2313bdb6300a5 Mon Sep 17 00:00:00 2001 From: roblabla Date: Fri, 25 Jan 2019 17:50:48 +0000 Subject: [PATCH 09/10] Start implementing NCA recreation --- src/format/nca.rs | 270 ++++++++++++++++++++++++++++------- src/format/nca/structures.rs | 6 +- src/pki.rs | 31 ++++ src/utils.rs | 16 +++ 4 files changed, 268 insertions(+), 55 deletions(-) diff --git a/src/format/nca.rs b/src/format/nca.rs index 37e31f3..558db38 100644 --- a/src/format/nca.rs +++ b/src/format/nca.rs @@ -33,9 +33,10 @@ use serde_derive::{Deserialize, Serialize}; use byteorder::{BE, ByteOrder}; use crate::utils::{align_down, TryClone, ReadRange}; -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone, Copy)] +#[repr(u8)] enum KeyType { - Application, Ocean, System + Application = 0, Ocean = 1, System = 2 } impl From for KeyType { @@ -49,7 +50,7 @@ impl From for KeyType { } } -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, Copy)] enum CryptoType { None, Xts, Ctr, Bktr } @@ -84,9 +85,10 @@ enum FsType { RomFs, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone, Copy)] +#[repr(u8)] enum ContentType { - Program, Meta, Control, Manual, Data, PublicData + Program = 0, Meta = 1, Control = 2, Manual = 3, Data = 4, PublicData = 5 } impl From for ContentType { @@ -103,7 +105,7 @@ impl From for ContentType { } } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone, Copy)] enum NcaFormat { Nca3, Nca2, Nca0 } @@ -117,7 +119,7 @@ pub struct SectionJson { nounce: u64, } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Clone, Copy)] #[repr(transparent)] pub struct TitleId(u64); @@ -128,14 +130,13 @@ impl std::fmt::Debug for TitleId { } #[derive(Debug, Serialize, Deserialize)] -struct NcaJson { +pub struct NcaJson { format: NcaFormat, sig: structures::SigDebug, npdm_sig: structures::SigDebug, is_gamecard: bool, content_type: ContentType, - key_revision1: u8, - key_revision2: u8, + key_revision: u8, key_type: KeyType, title_id: TitleId, sdk_version: u32, // TODO: Better format @@ -160,6 +161,13 @@ fn get_key_area_key(pki: &Keys, key_version: usize, key_type: KeyType) -> Result key.ok_or(Error::MissingKey(Box::leak(format!("key_area_key_application_{:02x}", key_version).into_boxed_str()), Backtrace::new())) } +// Crypto is stupid. First, we need to get the max of crypto_type and crypto_type2. +// Then, nintendo uses both 0 and 1 as master key 0, and then everything is shifted by one. +// So we sub by 1. +fn get_master_key_revision(crypto_type: u8, crypto_type2: u8) -> u8 { + max(crypto_type2, crypto_type).saturating_sub(1) +} + fn decrypt_header(pki: &Keys, file: &mut Read) -> Result { // Decrypt header. let mut header = [0; 0xC00]; @@ -208,10 +216,7 @@ impl Nca { // BODY: decrypting. Maybe put it behind a flag that accepts invalidly // BODY: signed NCAs? - // Crypto is stupid. First, we need to get the max of crypto_type and crypto_type2. - // Then, nintendo uses both 0 and 1 as master key 0, and then everything is shifted by one. - // So we sub by 1. - let master_key_revision = max(header.crypto_type2, header.crypto_type).saturating_sub(1); + let master_key_revision = get_master_key_revision(header.crypto_type, header.crypto_type2); // Handle Rights ID. let has_rights_id = header.rights_id != [0; 0x10]; @@ -270,10 +275,9 @@ impl Nca { npdm_sig: header.npdm_sig, is_gamecard: header.is_gamecard != 0, content_type: ContentType::from(header.content_type), - key_revision1: header.crypto_type, - key_revision2: header.crypto_type2, + key_revision: master_key_revision, key_type: KeyType::from(header.key_index), - title_id: TitleId(header.titleid), + title_id: TitleId(header.title_id), // TODO: Store the SDK version in a more human readable format. sdk_version: header.sdk_version, xts_key: decrypted_keys.0, @@ -288,7 +292,7 @@ impl Nca { } } -impl Nca { +impl Nca { pub fn raw_section(&self, id: usize) -> Result>, Error> { if let Some(section) = &self.json.sections[id] { // TODO: Nca::raw_section should reopen the file, not dup2 the handle. @@ -311,31 +315,100 @@ impl Nca { } } - pub fn section(&self, id: usize) -> Result>>, Error> { + pub fn section(&self, id: usize) -> Result>>, Error> { let mut raw_section = self.raw_section(id)?; - let start_offset = match raw_section.state.json.fstype { - FsType::Pfs0 { pfs0_offset, .. } => pfs0_offset, - _ => 0 + let (start_offset, size) = match raw_section.state.json.fstype { + FsType::Pfs0 { pfs0_offset, pfs0_size, .. } => (pfs0_offset, pfs0_size), + _ => (0, raw_section.state.json.size()) }; - raw_section.seek(io::SeekFrom::Start(start_offset))?; - Ok(VerificationStream { - stream: raw_section, - start_at: start_offset, - }) + raw_section.seek_aligned(io::SeekFrom::Start(start_offset)); + Ok(ReadRange::new(raw_section, start_offset, size)) } } impl Nca { - pub fn write_header(&self, file: &mut Write) -> Result<(), Error> { - unimplemented!() - } - pub fn write_json(&self, file: &mut Write) -> Result<(), Error> { serde_json::to_writer_pretty(file, &self.json).unwrap(); Ok(()) } } +impl Nca { + pub fn nca_writer(nca_json: NcaJson, output: W) -> Nca { + Nca { + stream: output, + json: nca_json, + } + } + + pub fn write_nca(&self, pki: &Keys, mut sections: [Option<&mut Read>; 4]) -> Result<(), Error> { + let key_area_key = get_key_area_key(pki, self.json.key_revision as _, self.json.key_type)?; + + let mut header = RawNca { + fixed_key_sig: self.json.sig, + npdm_sig: self.json.npdm_sig, + magic: match self.json.format { + NcaFormat::Nca3 => *b"NCA3", + NcaFormat::Nca2 => *b"NCA2", + NcaFormat::Nca0 => *b"NCA0", + }, + is_gamecard: self.json.is_gamecard as u8, + content_type: self.json.content_type as u8, + crypto_type: if self.json.key_revision == 0 { 0 } else { 2 }, + key_index: self.json.key_type as u8, + nca_size: 0, + title_id: self.json.title_id.0, + _padding0: SkipDebug(0), + sdk_version: self.json.sdk_version, + crypto_type2: if self.json.key_revision == 0 { 0 } else { self.json.key_revision + 1 }, + _padding1: SkipDebug([0; 0xF]), + rights_id: self.json.rights_id.unwrap_or([0; 0x10]), + section_entries: [RawSectionTableEntry { + media_start_offset: 0, + media_end_offset: 0, + unknown1: 0, + unknown2: 0, + }; 4], + section_hashes: [[0; 0x20]; 4], + encrypted_xts_key: key_area_key.encrypt_xts_key(&self.json.xts_key), + encrypted_ctr_key: key_area_key.encrypt_key(&self.json.ctr_key), + unknown_new_key: [0; 0x10], + _padding2: SkipDebug([0; 0xC0]), + fs_headers: [RawNcaFsHeader { + version: 0, + partition_type: RawPartitionType::RomFs, + fs_type: RawFsType::RomFs, + crypt_type: RawCryptType::Ctr, + _0x5: [0; 0x3], + superblock: RawSuperblock { + pfs0: RawPfs0Superblock { + master_hash: [0; 0x20], + block_size: 0, + always_2: 2, + hash_table_offset: 0, + hash_table_size: 0, + pfs0_offset: 0, + pfs0_size: 0, + _0x48: SkipDebug([0; 0xF0]), + } + }, + section_ctr: 0, + _0x148: [0; 0xB8] + }; 4] + }; + + for (idx, section) in sections.iter_mut().enumerate() { + if let Some(section) = section { + let mut section_writer = self.section(idx)?; + std::io::copy(section, &mut section_writer)?; + } + } + + Ok(()) + } + +} + impl SectionJson { fn start_offset(&self) -> u64 { self.media_start_offset as u64 * 0x200 @@ -365,10 +438,23 @@ struct CryptoStreamState { json: SectionJson, } +impl CryptoStream { + fn seek_aligned(&mut self, from: io::SeekFrom) { + let new_offset = match from { + io::SeekFrom::Start(cur) => cur, + io::SeekFrom::Current(val) => (self.state.offset as i64 + val) as u64, + io::SeekFrom::End(val) => (self.state.json.size() as i64 + val) as u64, + }; + if new_offset % 16 != 0 { + panic!("Seek not aligned"); + } + self.state.offset = new_offset; + } +} + impl CryptoStreamState { fn get_ctr(&self) -> [u8; 0x10] { - println!("{:x} {:x}", self.json.start_offset(), self.offset); - let offset = (self.json.start_offset() + self.offset) / 16; + let offset = self.json.start_offset() / 16 + self.offset / 16; let mut ctr = [0; 0x10]; // Write section nounce in Big Endian. BE::write_u64(&mut ctr[..8], self.json.nounce); @@ -377,10 +463,10 @@ impl CryptoStreamState { ctr } - fn decrypt(&mut self, buf: &mut [u8]) { + fn decrypt(&mut self, buf: &mut [u8]) -> Result<(), Error> { match self.json.crypto { CryptoType::Ctr => { - self.ctr_key.decrypt_ctr(buf, &self.get_ctr()); + self.ctr_key.decrypt_ctr(buf, &self.get_ctr()) }, CryptoType::Xts => { unimplemented!("XTS crypto") @@ -388,7 +474,22 @@ impl CryptoStreamState { CryptoType::Bktr => { unimplemented!("Bktr crypto") } - CryptoType::None => () + CryptoType::None => Ok(()) + } + } + + fn encrypt(&mut self, buf: &mut [u8]) -> Result<(), Error> { + match self.json.crypto { + CryptoType::Ctr => { + self.ctr_key.encrypt_ctr(buf, &self.get_ctr()) + }, + CryptoType::Xts => { + unimplemented!("XTS crypto") + }, + CryptoType::Bktr => { + unimplemented!("Bktr crypto") + } + CryptoType::None => Ok(()) } } } @@ -397,7 +498,7 @@ impl CryptoStreamState { impl Read for CryptoStream { fn read(&mut self, mut buf: &mut [u8]) -> io::Result { let previous_leftovers = (self.state.offset % 16) as usize; - if previous_leftovers != 0 { + let previous_leftovers_written = if previous_leftovers != 0 { // First, handle leftovers from a previous read call, so we go back // to a properly block-aligned read. let to = min(previous_leftovers + buf.len(), 16); @@ -406,14 +507,15 @@ impl Read for CryptoStream { self.state.offset += size as u64; buf = &mut buf[size..]; - } + size + } else { 0 }; let read = self.stream.read(buf)?; buf = &mut buf[..read]; // Decrypt all the non-leftover bytes. let len_no_leftovers = align_down(buf.len(), 16); - self.state.decrypt(&mut buf[..len_no_leftovers]); + self.state.decrypt(&mut buf[..len_no_leftovers]).unwrap(); self.state.offset += len_no_leftovers as u64; let leftovers = buf.len() % 16; if leftovers != 0 { @@ -427,12 +529,64 @@ impl Read for CryptoStream { let from = align_down(buf.len(), 16); self.buffer[..leftovers].copy_from_slice(&buf[from..buf.len()]); self.stream.read_exact(&mut self.buffer[leftovers..])?; - self.state.decrypt(&mut self.buffer); + // TODO: Bubble up the error. + self.state.decrypt(&mut self.buffer).unwrap(); buf[from..].copy_from_slice(&self.buffer[..leftovers]); self.state.offset += leftovers as u64; } - Ok(buf.len() + if previous_leftovers != 0 { (16 - previous_leftovers) } else { 0 }) + Ok(previous_leftovers_written + read) + } +} + +impl Write for CryptoStream { + fn write(&mut self, mut buf: &[u8]) -> io::Result { + let previous_leftovers = (self.state.offset % 16) as usize; + let previous_leftovers_written = if previous_leftovers != 0 { + // We need to do two things: Rewrite the block on disk with the + // encrypted data, and update the leftover buffer with the decrypted + // data. + let to = min(previous_leftovers + buf.len(), 16); + let size = to - previous_leftovers; + self.buffer[previous_leftovers..to].copy_from_slice(&buf[..size]); + + if to == 16 { + // We are done handling this block. Write it to disk. + // TODO: Bubble up the error. + self.state.encrypt(&mut self.buffer).unwrap(); + self.stream.write_all(&self.buffer)?; + } + + self.state.offset += size as u64; + + buf = &buf[size..]; + size + } else { 0 }; + + // Encrypt chunk by chunk + for chunk in buf.chunks_exact(16) { + self.buffer.copy_from_slice(chunk); + self.state.encrypt(&mut self.buffer).unwrap(); + self.stream.write_all(&self.buffer)?; + self.state.offset += 16 + } + + // Store all leftover bytes. + let leftovers = buf.len() % 16; + if leftovers != 0 { + // We got some leftover, save them in the internal buffer so they can + // be processed in a subsequent write. Note that this will not work + // at all if you mix reads and writes... + let from = align_down(buf.len(), 16); + self.buffer[..leftovers].copy_from_slice(&buf[from..buf.len()]); + self.state.offset += leftovers as u64; + } + + Ok(previous_leftovers + buf.len()) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) } } @@ -445,38 +599,50 @@ impl Seek for CryptoStream { }; let aligned_offset = align_down(self.state.offset, 16); - self.stream.seek(io::SeekFrom::Start(aligned_offset)); + self.stream.seek(io::SeekFrom::Start(aligned_offset))?; if self.state.offset % 16 != 0 { self.stream.read_exact(&mut self.buffer)?; - self.state.decrypt(&mut self.buffer); + self.state.decrypt(&mut self.buffer).unwrap(); } Ok(self.state.offset) } } -// TODO: Make VerificationStream actually verify the data it reads. -// BODY: VerificationStream should verify the body based on the hash table -// BODY: located with the help of the superblock. It will need a different -// BODY: implementation for PFS0 and RomFs (and maybe Bktr?). -pub struct VerificationStream { +/*pub struct VerificationStream { stream: R, - start_at: u64, + hashes_start: u64, + data_off: u64, + data_size: u64, +} + +impl VerificationStream { + fn new(stream: R, hashes_start: u64, data_start: u64, data_size: u64) -> Self { + VerificationStream { + stream, + hashes_start, + data_off: data_start, + data_size + } + } } impl Read for VerificationStream { fn read(&mut self, buf: &mut [u8]) -> io::Result { - self.stream.read(buf) + self.stream.seek(SeekFrom::Start(data_off))?; + self.stream.read(buf); } } -/* + impl Seek for VerificationStream { fn seek(&mut self, from: io::SeekFrom) -> io::Result { let from = match from { io::SeekFrom::Start(cur) => io::SeekFrom::Start(self.start_at + cur), io::SeekFrom::Current(val) => io::SeekFrom::Start(self.pos + val), io::SeekFrom::End(val) => io::SeekFrom::Start(val), - } + }; self.stream.seek(self.start_at + from) } } + +impl */ diff --git a/src/format/nca/structures.rs b/src/format/nca/structures.rs index 769f0cb..ca19ff1 100644 --- a/src/format/nca/structures.rs +++ b/src/format/nca/structures.rs @@ -10,9 +10,9 @@ use plain::Plain; #[repr(transparent)] #[derive(Clone, Copy)] -pub struct SigDebug([u8; 0x100]); +pub struct SigDebug(pub [u8; 0x100]); #[derive(Clone, Copy)] -pub struct SkipDebug(T); +pub struct SkipDebug(pub T); impl fmt::Debug for SkipDebug { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { @@ -34,7 +34,7 @@ pub struct RawNca { pub crypto_type: u8, pub key_index: u8, pub nca_size: u64, - pub titleid: u64, + pub title_id: u64, pub _padding0: SkipDebug, pub sdk_version: u32, pub crypto_type2: u8, diff --git a/src/pki.rs b/src/pki.rs index 2c7f9d3..e17b5d6 100644 --- a/src/pki.rs +++ b/src/pki.rs @@ -73,6 +73,18 @@ impl Aes128Key { Ok(()) } + pub fn encrypt_ctr(&self, buf: &mut [u8], ctr: &[u8; 0x10]) -> Result<(), Error> { + if buf.len() % 16 != 0 { + return Err(Error::Crypto(String::from("buf length should be a multiple of 16, the size of an AES block."), Backtrace::new())); + } + + let key = GenericArray::from_slice(&self.0); + let iv = GenericArray::from_slice(ctr); + let mut crypter = Ctr128::::new_fixkey(key, iv); + crypter.encrypt_nopad(buf)?; + Ok(()) + } + pub fn derive_key(&self, source: &[u8; 0x10]) -> Result { let mut newkey = *source; @@ -82,6 +94,15 @@ impl Aes128Key { Ok(Aes128Key(newkey)) } + pub fn encrypt_key(&self, key: &Aes128Key) -> [u8; 0x10] { + let mut newkey = key.0; + + let crypter = Aes128::new(GenericArray::from_slice(&self.0)); + crypter.encrypt_block(GenericArray::from_mut_slice(&mut newkey)); + + newkey + } + pub fn derive_xts_key(&self, source: &[u8; 0x20]) -> Result { let mut newkey = *source; @@ -91,6 +112,16 @@ impl Aes128Key { Ok(AesXtsKey(newkey)) } + + pub fn encrypt_xts_key(&self, source: &AesXtsKey) -> [u8; 0x20] { + let mut newkey = source.0; + + let crypter = Aes128::new(GenericArray::from_slice(&self.0)); + crypter.encrypt_block(GenericArray::from_mut_slice(&mut newkey[0x00..0x10])); + crypter.encrypt_block(GenericArray::from_mut_slice(&mut newkey[0x10..0x20])); + + newkey + } } fn get_tweak(mut sector: usize) -> [u8; 0x10] { diff --git a/src/utils.rs b/src/utils.rs index 41e8971..24451e1 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -152,6 +152,22 @@ impl io::Read for ReadRange { } } +impl io::Write for ReadRange { + fn write(&mut self, mut buf: &[u8]) -> io::Result { + if self.size < self.inner_pos + buf.len() as u64 { + // Avoid writing out of the section's bound. + buf = &buf[..(self.size.saturating_sub(self.inner_pos)) as usize]; + } + let write = self.inner.write(buf)?; + self.inner_pos += write as u64; + Ok(write) + } + + fn flush(&mut self) -> io::Result<()> { + self.inner.flush() + } +} + impl io::Seek for ReadRange { fn seek(&mut self, from: io::SeekFrom) -> io::Result { let new_inner_pos = match from { From 2d2206bfe287897bcb65923b290799527ca9b3e3 Mon Sep 17 00:00:00 2001 From: roblabla Date: Sun, 27 Jan 2019 00:37:49 +0000 Subject: [PATCH 10/10] It works, kinda. --- src/bin/linkle_clap.rs | 86 +++++++++- src/format/nca.rs | 294 +++++++++++++++++++++++++++-------- src/format/nca/structures.rs | 7 +- src/pki.rs | 37 +++-- src/utils.rs | 18 ++- 5 files changed, 358 insertions(+), 84 deletions(-) diff --git a/src/bin/linkle_clap.rs b/src/bin/linkle_clap.rs index 41bec97..86e4079 100644 --- a/src/bin/linkle_clap.rs +++ b/src/bin/linkle_clap.rs @@ -5,6 +5,7 @@ extern crate linkle; use std::fs::{OpenOptions, File}; use std::path::{Path, PathBuf}; use std::process; +use std::io::Read; use structopt::StructOpt; use linkle::error::ResultExt; @@ -97,6 +98,42 @@ enum Opt { #[structopt(short = "d", long = "dev")] dev: bool, + /// Keyfile + #[structopt(parse(from_os_str), short = "k", long = "keyset")] + keyfile: Option, + }, + + /// Create an NCA file. + #[structopt(name = "nca")] + Nca { + /// The input JSON NCA header to create this NCA from. + #[structopt(parse(from_os_str), long = "header-json")] + header_file: PathBuf, + + /// The output NCA location + #[structopt(parse(from_os_str))] + output_file: PathBuf, + + /// Sets the output file to extract the section0 to. + #[structopt(parse(from_os_str), long = "section0")] + section0_file: Option, + + /// Sets the output file to extract the section1 to. + #[structopt(parse(from_os_str), long = "section1")] + section1_file: Option, + + /// Sets the output file to extract the section2 to. + #[structopt(parse(from_os_str), long = "section2")] + section2_file: Option, + + /// Sets the output file to extract the section3 to. + #[structopt(parse(from_os_str), long = "section3")] + section3_file: Option, + + /// Use development keys instead of retail + #[structopt(short = "d", long = "dev")] + dev: bool, + /// Keyfile #[structopt(parse(from_os_str), short = "k", long = "keyset")] keyfile: Option, @@ -172,8 +209,7 @@ fn extract_nca(input_file: &Path, is_dev: bool, key_path: Option<&Path>, } else { linkle::pki::Keys::new_retail(key_path).unwrap() }; - println!("{:#?}", keys); - /*let nca = linkle::format::nca::Nca::from_file(&keys, File::open(input_file)?).unwrap(); + let nca = linkle::format::nca::Nca::from_file(&keys, File::open(input_file)?).unwrap(); if let Some(output_header_json) = output_header_json { let mut output_header_json = File::create(output_header_json)?; nca.write_json(&mut output_header_json).unwrap(); @@ -197,7 +233,42 @@ fn extract_nca(input_file: &Path, is_dev: bool, key_path: Option<&Path>, let mut output_section3 = File::create(output_section3)?; let mut section = nca.section(3).unwrap(); std::io::copy(&mut section, &mut output_section3)?; - }*/ + } + Ok(()) +} + +fn create_nca(is_dev: bool, key_path: Option<&Path>, header_file: &Path, + output_nca: &Path, + section0_path: Option<&Path>, section1_path: Option<&Path>, + section2_path: Option<&Path>, section3_path: Option<&Path>) -> Result<(), linkle::error::Error> { + let keys = if is_dev { + linkle::pki::Keys::new_dev(key_path).unwrap() + } else { + linkle::pki::Keys::new_retail(key_path).unwrap() + }; + let nca: linkle::format::nca::NcaJson = serde_json::from_reader(File::open(header_file)?).unwrap(); + let mut output_nca = linkle::format::nca::Nca::nca_writer(nca, File::create(output_nca)?, &keys)?; + if let Some(section_path) = section0_path { + let mut section = output_nca.section(0)?; + std::io::copy(&mut File::open(section_path)?, &mut section)?; + section.finalize()?; + } + if let Some(section_path) = section1_path { + let mut section = output_nca.section(1)?; + std::io::copy(&mut File::open(section_path)?, &mut section)?; + section.finalize()?; + } + if let Some(section_path) = section2_path { + let mut section = output_nca.section(2)?; + std::io::copy(&mut File::open(section_path)?, &mut section)?; + section.finalize()?; + } + if let Some(section_path) = section3_path { + let mut section = output_nca.section(3)?; + std::io::copy(&mut File::open(section_path)?, &mut section)?; + section.finalize()?; + } + output_nca.finalize()?; Ok(()) } @@ -211,10 +282,17 @@ fn process_args(app: &Opt) { Opt::NcaExtract { ref input_file, ref header_file, ref section0_file, ref section1_file, ref section2_file, ref section3_file, dev, ref keyfile } => extract_nca(input_file, *dev, to_opt_ref(keyfile), to_opt_ref(header_file), to_opt_ref(section0_file), to_opt_ref(section1_file), to_opt_ref(section2_file), to_opt_ref(section3_file)), + Opt::Nca { ref header_file, ref output_file, ref section0_file, + ref section1_file, ref section2_file, + ref section3_file, dev, ref keyfile } => create_nca(*dev, to_opt_ref(keyfile), header_file, output_file, to_opt_ref(section0_file), to_opt_ref(section1_file), to_opt_ref(section2_file), to_opt_ref(section3_file)), }; if let Err(e) = res { - println!("Error: {}", e); + if let Ok(_) = std::env::var("RUST_BACKTRACE") { + println!("Error: {:?}", e); + } else { + println!("Error: {}", e); + } process::exit(1) } } diff --git a/src/format/nca.rs b/src/format/nca.rs index 558db38..1772e4c 100644 --- a/src/format/nca.rs +++ b/src/format/nca.rs @@ -31,7 +31,8 @@ use std::cmp::{min, max}; use plain::Plain; use serde_derive::{Deserialize, Serialize}; use byteorder::{BE, ByteOrder}; -use crate::utils::{align_down, TryClone, ReadRange}; +use crate::utils::{align_down, align_up, TryClone, ReadRange}; +use sha2::{Sha256, Digest}; #[derive(Debug, Serialize, Deserialize, Clone, Copy)] #[repr(u8)] @@ -51,8 +52,20 @@ impl From for KeyType { } #[derive(Debug, Serialize, Deserialize, Clone, Copy)] +#[repr(u8)] enum CryptoType { - None, Xts, Ctr, Bktr + None = 0, Xts = 1, Ctr = 2, Bktr = 3 +} + +impl From for RawCryptType { + fn from(from: CryptoType) -> RawCryptType { + match from { + CryptoType::None => RawCryptType::None, + CryptoType::Xts => RawCryptType::Xts, + CryptoType::Ctr => RawCryptType::Ctr, + CryptoType::Bktr => RawCryptType::Bktr, + } + } } impl From for CryptoType { @@ -114,6 +127,8 @@ enum NcaFormat { pub struct SectionJson { media_start_offset: u32, media_end_offset: u32, + unknown1: u32, + unknown2: u32, crypto: CryptoType, fstype: FsType, nounce: u64, @@ -138,6 +153,7 @@ pub struct NcaJson { content_type: ContentType, key_revision: u8, key_type: KeyType, + nca_size: u64, title_id: TitleId, sdk_version: u32, // TODO: Better format xts_key: AesXtsKey, @@ -178,18 +194,21 @@ fn decrypt_header(pki: &Keys, file: &mut Read) -> Result { // TODO: Check if NCA is already decrypted let header_key = pki.header_key().as_ref().ok_or(Error::MissingKey("header_key", Backtrace::new()))?; - header_key.decrypt(&header[..0x400], &mut decrypted_header[..0x400], 0, 0x200)?; + decrypted_header[..0x400].copy_from_slice(&header[..0x400]); + header_key.decrypt(&mut decrypted_header[..0x400], 0, 0x200)?; let raw_nca = *RawNca::from_bytes(&decrypted_header).expect("RawNca to be of the right size"); match &raw_nca.magic { b"NCA3" => { - header_key.decrypt(&header, &mut decrypted_header, 0, 0x200)?; + decrypted_header.copy_from_slice(&header); + header_key.decrypt(&mut decrypted_header, 0, 0x200)?; }, b"NCA2" => { for (i, fsheader) in raw_nca.fs_headers.iter().enumerate() { let offset = 0x400 + i * 0x200; - if fsheader._0x148[0] != 0 || fsheader._0x148[..0xB7] != fsheader._0x148[1..] { - header_key.decrypt(&header[offset..offset + 0x200], &mut decrypted_header[offset..offset + 0x200], 0, 0x200)?; + if &fsheader._0x148[..] != &[0; 0xB8][..] { + decrypted_header[offset..offset + 0x200].copy_from_slice(&header[offset..offset + 0x200]); + header_key.decrypt(&mut decrypted_header[offset..offset + 0x200], 0, 0x200)?; } else { decrypted_header[offset..offset + 0x200].copy_from_slice(&[0; 0x200]); } @@ -201,6 +220,24 @@ fn decrypt_header(pki: &Keys, file: &mut Read) -> Result { Ok(*RawNca::from_bytes(&decrypted_header).expect("RawNca to be of the right size")) } +fn encrypt_header<'a>(pki: &Keys, header: &'a mut RawNca) -> Result<&'a [u8], Error> { + let header_key = pki.header_key().as_ref().ok_or(Error::MissingKey("header_key", Backtrace::new()))?; + + let header_bytes = match &header.magic { + b"NCA3" => { + let mut header_bytes = unsafe { + // Safety: RawNca has no padding + plain::as_mut_bytes(header) + }; + header_key.encrypt(&mut header_bytes, 0, 0x200)?; + header_bytes + }, + _ => unimplemented!() + }; + + Ok(header_bytes) +} + impl Nca { pub fn from_file(pki: &Keys, mut file: R) -> Result, Error> { let header = decrypt_header(pki, &mut file)?; @@ -261,6 +298,8 @@ impl Nca { nounce: fs.section_ctr, media_start_offset: section.media_start_offset, media_end_offset: section.media_end_offset, + unknown1: section.unknown1, + unknown2: section.unknown2, }); } } @@ -277,6 +316,7 @@ impl Nca { content_type: ContentType::from(header.content_type), key_revision: master_key_revision, key_type: KeyType::from(header.key_index), + nca_size: header.nca_size, title_id: TitleId(header.title_id), // TODO: Store the SDK version in a more human readable format. sdk_version: header.sdk_version, @@ -315,14 +355,15 @@ impl Nca { } } - pub fn section(&self, id: usize) -> Result>>, Error> { + pub fn section(&self, id: usize) -> Result>>, Error> { let mut raw_section = self.raw_section(id)?; let (start_offset, size) = match raw_section.state.json.fstype { FsType::Pfs0 { pfs0_offset, pfs0_size, .. } => (pfs0_offset, pfs0_size), _ => (0, raw_section.state.json.size()) }; raw_section.seek_aligned(io::SeekFrom::Start(start_offset)); - Ok(ReadRange::new(raw_section, start_offset, size)) + let json = raw_section.state.json.clone(); + Ok(VerificationStream::new(raw_section, json)) } } @@ -334,14 +375,41 @@ impl Nca { } impl Nca { - pub fn nca_writer(nca_json: NcaJson, output: W) -> Nca { - Nca { + pub fn nca_writer(nca_json: NcaJson, output: W, pki: &Keys) -> Result, Error> { + let mut nca = Nca { stream: output, json: nca_json, + }; + nca.write_header(pki)?; + Ok(nca) + } + + pub fn finalize(mut self) -> Result<(), Error> { + for (idx, section) in self.json.sections.iter().enumerate() { + if let Some(section) = section { + let mut io = self.raw_section(idx)?; + match section.fstype { + FsType::Pfs0 { hash_table_offset, hash_table_size, + pfs0_offset, pfs0_size, .. } => { + let hash_table_end = hash_table_offset + hash_table_size; + io.seek_aligned(io::SeekFrom::Start(hash_table_end))?; + let data = vec![0; (pfs0_offset - hash_table_end) as usize]; + io.write_all(&data)?; + + let pfs0_end = align_up(pfs0_offset + pfs0_size, 16); + let pfs0_absolute_end = pfs0_end + section.media_start_offset as u64 * 512; + io.seek_aligned(io::SeekFrom::Start(pfs0_end))?; + let data = vec![0; (section.media_end_offset as u64 * 512 - pfs0_absolute_end) as usize]; + io.write_all(&data)?; + }, + _ => unimplemented!() + } + } } + Ok(()) } - pub fn write_nca(&self, pki: &Keys, mut sections: [Option<&mut Read>; 4]) -> Result<(), Error> { + fn write_header(&mut self, pki: &Keys) -> Result<(), Error> { let key_area_key = get_key_area_key(pki, self.json.key_revision as _, self.json.key_type)?; let mut header = RawNca { @@ -356,7 +424,7 @@ impl Nca { content_type: self.json.content_type as u8, crypto_type: if self.json.key_revision == 0 { 0 } else { 2 }, key_index: self.json.key_type as u8, - nca_size: 0, + nca_size: self.json.nca_size, title_id: self.json.title_id.0, _padding0: SkipDebug(0), sdk_version: self.json.sdk_version, @@ -369,44 +437,69 @@ impl Nca { unknown1: 0, unknown2: 0, }; 4], - section_hashes: [[0; 0x20]; 4], + section_hashes: [[0; 0x20]; 4], // Derived from fs_headers encrypted_xts_key: key_area_key.encrypt_xts_key(&self.json.xts_key), encrypted_ctr_key: key_area_key.encrypt_key(&self.json.ctr_key), - unknown_new_key: [0; 0x10], + unknown_new_key: key_area_key.encrypt_key(&Aes128Key([0; 0x10])), _padding2: SkipDebug([0; 0xC0]), fs_headers: [RawNcaFsHeader { version: 0, - partition_type: RawPartitionType::RomFs, - fs_type: RawFsType::RomFs, - crypt_type: RawCryptType::Ctr, + partition_type: RawPartitionType(0), + fs_type: RawFsType(0), + crypt_type: RawCryptType(0), _0x5: [0; 0x3], superblock: RawSuperblock { - pfs0: RawPfs0Superblock { - master_hash: [0; 0x20], - block_size: 0, - always_2: 2, - hash_table_offset: 0, - hash_table_size: 0, - pfs0_offset: 0, - pfs0_size: 0, - _0x48: SkipDebug([0; 0xF0]), - } + raw: [0; 0x138], }, section_ctr: 0, _0x148: [0; 0xB8] }; 4] }; - for (idx, section) in sections.iter_mut().enumerate() { + for (idx, section) in self.json.sections.iter().enumerate() { if let Some(section) = section { - let mut section_writer = self.section(idx)?; - std::io::copy(section, &mut section_writer)?; + header.section_entries[idx].media_start_offset = section.media_start_offset; + header.section_entries[idx].media_end_offset = section.media_end_offset; + header.section_entries[idx].unknown1 = section.unknown1; + header.section_entries[idx].unknown2 = section.unknown2; + + header.fs_headers[idx].crypt_type = section.crypto.into(); + header.fs_headers[idx].section_ctr = section.nounce; + + match section.fstype { + FsType::Pfs0 { master_hash, block_size, hash_table_offset, + hash_table_size, pfs0_offset, pfs0_size } => { + header.fs_headers[idx].version = 2; + header.fs_headers[idx].partition_type = RawPartitionType::Pfs0; + header.fs_headers[idx].fs_type = RawFsType::Pfs0; + + header.fs_headers[idx].superblock = RawSuperblock { + pfs0: RawPfs0Superblock { + master_hash: master_hash.0, block_size, + always_2: 2, + hash_table_offset, hash_table_size, + pfs0_offset, pfs0_size, + _0x48: SkipDebug([0; 0xF0]) + } + }; + }, + _ => unimplemented!() + } + + let fs_header_bytes = unsafe { + // Safety: RawNcaFsHeader has no padding. + plain::as_bytes(&header.fs_headers[idx]) + }; + let hash = Sha256::digest(fs_header_bytes); + header.section_hashes[idx].copy_from_slice(hash.as_slice()); } } + let header_bytes = encrypt_header(pki, &mut header)?; + self.stream.write_all(header_bytes)?; + Ok(()) } - } impl SectionJson { @@ -438,8 +531,8 @@ struct CryptoStreamState { json: SectionJson, } -impl CryptoStream { - fn seek_aligned(&mut self, from: io::SeekFrom) { +impl CryptoStream { + fn seek_aligned(&mut self, from: io::SeekFrom) -> std::io::Result<()> { let new_offset = match from { io::SeekFrom::Start(cur) => cur, io::SeekFrom::Current(val) => (self.state.offset as i64 + val) as u64, @@ -448,7 +541,9 @@ impl CryptoStream { if new_offset % 16 != 0 { panic!("Seek not aligned"); } + self.stream.seek(io::SeekFrom::Start(new_offset))?; self.state.offset = new_offset; + Ok(()) } } @@ -539,7 +634,7 @@ impl Read for CryptoStream { } } -impl Write for CryptoStream { +impl Write for CryptoStream { fn write(&mut self, mut buf: &[u8]) -> io::Result { let previous_leftovers = (self.state.offset % 16) as usize; let previous_leftovers_written = if previous_leftovers != 0 { @@ -550,11 +645,16 @@ impl Write for CryptoStream { let size = to - previous_leftovers; self.buffer[previous_leftovers..to].copy_from_slice(&buf[..size]); - if to == 16 { - // We are done handling this block. Write it to disk. - // TODO: Bubble up the error. - self.state.encrypt(&mut self.buffer).unwrap(); - self.stream.write_all(&self.buffer)?; + // We are done handling this block. Write it to disk. + // TODO: Bubble up the error. + self.state.encrypt(&mut self.buffer).unwrap(); + self.stream.write_all(&self.buffer)?; + self.state.decrypt(&mut self.buffer).unwrap(); + + if to != 16 { + self.stream.seek(io::SeekFrom::Current(-16))?; + } else { + self.buffer = [0; 16]; } self.state.offset += size as u64; @@ -578,7 +678,12 @@ impl Write for CryptoStream { // be processed in a subsequent write. Note that this will not work // at all if you mix reads and writes... let from = align_down(buf.len(), 16); + self.buffer = [0; 16]; self.buffer[..leftovers].copy_from_slice(&buf[from..buf.len()]); + self.state.encrypt(&mut self.buffer).unwrap(); + self.stream.write_all(&self.buffer)?; + self.state.decrypt(&mut self.buffer).unwrap(); + self.stream.seek(io::SeekFrom::Current(-16))?; self.state.offset += leftovers as u64; } @@ -590,7 +695,7 @@ impl Write for CryptoStream { } } -impl Seek for CryptoStream { +impl Seek for CryptoStream { fn seek(&mut self, from: io::SeekFrom) -> io::Result { self.state.offset = match from { io::SeekFrom::Start(cur) => cur, @@ -608,41 +713,108 @@ impl Seek for CryptoStream { } } -/*pub struct VerificationStream { - stream: R, - hashes_start: u64, - data_off: u64, - data_size: u64, +pub struct VerificationStream { + stream: ReadRange, + section: SectionJson, + cur_off: u64, + curblock: [u8; 4096], // Hopefully a block isn't ever bigger than that... } impl VerificationStream { - fn new(stream: R, hashes_start: u64, data_start: u64, data_size: u64) -> Self { + fn new(stream: R, section: SectionJson) -> Self { + let (start_offset, size) = match section.fstype { + FsType::Pfs0 { pfs0_offset, pfs0_size, .. } => (pfs0_offset, pfs0_size), + _ => (0, section.size()), + }; VerificationStream { - stream, - hashes_start, - data_off: data_start, - data_size + stream: ReadRange::new(stream, start_offset, size), + section, + cur_off: 0, + curblock: [0; 4096], } } } impl Read for VerificationStream { fn read(&mut self, buf: &mut [u8]) -> io::Result { - self.stream.seek(SeekFrom::Start(data_off))?; - self.stream.read(buf); + self.stream.read(buf) + // TODO: Now, verify the buffer. + } +} + +fn write_hash(section: &SectionJson, cur_off: u64, stream: &mut ReadRange, block: &[u8]) -> io::Result<()> { + match section.fstype { + FsType::Pfs0 { hash_table_offset, .. } => { + let hash = Sha256::digest(block); + let hash_pos = hash_table_offset + 0x20 * (cur_off / 4096); + stream.as_inner_mut().seek(io::SeekFrom::Start(hash_pos)).unwrap(); + stream.as_inner_mut().write_all(hash.as_slice()).unwrap(); + }, + _ => unimplemented!() + } + Ok(()) +} +impl VerificationStream { + pub fn finalize(mut self) -> Result<(), Error> { + if self.cur_off % 4096 != 0 { + // there are leftovers, write them. + let curblock_off = (self.cur_off % 4096) as usize; + self.stream.write_all(&self.curblock[..curblock_off])?; + write_hash(&self.section, self.cur_off, &mut self.stream, &self.curblock[..curblock_off])?; + } + Ok(()) + } +} + +impl Write for VerificationStream { + fn write(&mut self, mut buf: &[u8]) -> io::Result { + let buflen = buf.len(); + if self.cur_off % 4096 != 0 { + // First, handle leftovers. + let curblock_off = (self.cur_off % 4096) as usize; + let leftovers = self.curblock[curblock_off..].len(); + self.curblock[curblock_off..].copy_from_slice(&buf[..leftovers]); + + if curblock_off + leftovers == 4096 { + self.stream.write_all(&self.curblock)?; + let pos = self.stream.pos_in_stream(); + write_hash(&self.section, self.cur_off, &mut self.stream, &self.curblock)?; + self.stream.as_inner_mut().seek(io::SeekFrom::Start(pos)).unwrap(); + } + self.cur_off += leftovers as u64; + buf = &buf[leftovers..]; + } + self.stream.write_all(&buf[..align_down(buf.len(), 4096)])?; + let chunks_exact = buf.chunks_exact(4096); + for block in chunks_exact { + let pos = self.stream.pos_in_stream(); + write_hash(&self.section, self.cur_off, &mut self.stream, block)?; + self.stream.as_inner_mut().seek(io::SeekFrom::Start(pos)).unwrap(); + self.cur_off += 4096; + } + + let remainder = buf.chunks_exact(4096).remainder(); + self.curblock[..remainder.len()].copy_from_slice(remainder); + self.cur_off += remainder.len() as u64; + Ok(buflen) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) } } impl Seek for VerificationStream { fn seek(&mut self, from: io::SeekFrom) -> io::Result { - let from = match from { - io::SeekFrom::Start(cur) => io::SeekFrom::Start(self.start_at + cur), - io::SeekFrom::Current(val) => io::SeekFrom::Start(self.pos + val), - io::SeekFrom::End(val) => io::SeekFrom::Start(val), - }; - self.stream.seek(self.start_at + from) + unimplemented!(); + /*let seek_pos = self.stream.seek(from)?; + if seek_pos % 4096 == 0 { + // It's all fine, let's reset curblock to full 0s. + self.curblock = [0; 4096]; + } else { + self.stream.seek(io::SeekFrom::Start(align_down(seek_pos, 4096)))?; + self.stream.read_exact(&mut self.curblock)?; + self.stream.seek(io::SeekFrom::Start(seek_pos))?; + }*/ } } - -impl -*/ diff --git a/src/format/nca/structures.rs b/src/format/nca/structures.rs index ca19ff1..0f5ace0 100644 --- a/src/format/nca/structures.rs +++ b/src/format/nca/structures.rs @@ -70,6 +70,7 @@ pub union RawSuperblock { //romfs_superblock: RomfsSuperblock, //bktrs_superblock: BktrSuperblock, //nca0_romfs_superblock: Nca0RomfsSuperblock, + pub raw: [u8; 0x138] } assert_eq_size!(assert_superblock_size; RawSuperblock, [u8; 0x138]); @@ -89,21 +90,21 @@ assert_eq_size!(assert_nca_fs_header_size; RawNcaFsHeader, [u8; 0x148 + 0xB8]); enum_with_val! { #[derive(Clone, Copy)] - pub struct RawPartitionType(u8) { + pub struct RawPartitionType(pub u8) { RomFs = 0, Pfs0 = 1 } } enum_with_val! { #[derive(Clone, Copy)] - pub struct RawFsType(u8) { + pub struct RawFsType(pub u8) { Pfs0 = 2, RomFs = 3 } } enum_with_val! { #[derive(Clone, Copy)] - pub struct RawCryptType(u8) { + pub struct RawCryptType(pub u8) { None = 1, Xts = 2, Ctr = 3, Bktr = 4 } } diff --git a/src/pki.rs b/src/pki.rs index e17b5d6..d1a0224 100644 --- a/src/pki.rs +++ b/src/pki.rs @@ -15,12 +15,12 @@ use getset::Getters; use cmac::crypto_mac::Mac; #[derive(Clone, Copy)] -pub struct Aes128Key([u8; 0x10]); +pub struct Aes128Key(pub [u8; 0x10]); #[derive(Clone, Copy)] -pub struct AesXtsKey([u8; 0x20]); -pub struct EncryptedKeyblob([u8; 0xB0]); -pub struct Keyblob([u8; 0x90]); -pub struct Modulus([u8; 0x100]); +pub struct AesXtsKey(pub [u8; 0x20]); +pub struct EncryptedKeyblob(pub [u8; 0xB0]); +pub struct Keyblob(pub [u8; 0x90]); +pub struct Modulus(pub [u8; 0x100]); impl_debug_deserialize_serialize_hexstring!(Aes128Key); impl_debug_deserialize_serialize_hexstring!(AesXtsKey); @@ -134,22 +134,35 @@ fn get_tweak(mut sector: usize) -> [u8; 0x10] { } impl AesXtsKey { - pub fn decrypt(&self, src: &[u8], dst: &mut [u8], mut sector: usize, sector_size: usize) -> Result<(), Error> { - if src.len() != dst.len() { - return Err(Error::Crypto(String::from("Src len different from dst len"), Backtrace::new())); + pub fn decrypt(&self, data: &mut [u8], mut sector: usize, sector_size: usize) -> Result<(), Error> { + if data.len() % sector_size != 0 { + return Err(Error::Crypto(String::from("Length must be multiple of sectors!"), Backtrace::new())); + } + + for i in (0..data.len()).step_by(sector_size){ + let tweak = get_tweak(sector); + + let key1 = Aes128::new(GenericArray::from_slice(&self.0[0x00..0x10])); + let key2 = Aes128::new(GenericArray::from_slice(&self.0[0x10..0x20])); + let mut crypter = Xts128::::new(key1, key2, GenericArray::from_slice(&tweak)); + crypter.decrypt_nopad(&mut data[i..i + sector_size])?; + sector += 1; } - if src.len() % sector_size != 0 { + Ok(()) + } + + pub fn encrypt(&self, data: &mut [u8], mut sector: usize, sector_size: usize) -> Result<(), Error> { + if data.len() % sector_size != 0 { return Err(Error::Crypto(String::from("Length must be multiple of sectors!"), Backtrace::new())); } - dst.copy_from_slice(src); - for i in (0..src.len()).step_by(sector_size){ + for i in (0..data.len()).step_by(sector_size) { let tweak = get_tweak(sector); let key1 = Aes128::new(GenericArray::from_slice(&self.0[0x00..0x10])); let key2 = Aes128::new(GenericArray::from_slice(&self.0[0x10..0x20])); let mut crypter = Xts128::::new(key1, key2, GenericArray::from_slice(&tweak)); - crypter.decrypt_nopad(&mut dst[i..i + sector_size])?; + crypter.encrypt_nopad(&mut data[i..i + sector_size])?; sector += 1; } Ok(()) diff --git a/src/utils.rs b/src/utils.rs index 24451e1..d1af203 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -5,12 +5,12 @@ use std::io; // TODO: Document #[macro_export] macro_rules! enum_with_val { - ($(#[$meta:meta])* $vis:vis struct $ident:ident($ty:ty) { + ($(#[$meta:meta])* $vis:vis struct $ident:ident($consvis:vis $ty:ty) { $($variant:ident = $num:expr),* $(,)* }) => { $(#[$meta])* #[derive(PartialEq, Eq)] - $vis struct $ident($ty); + $vis struct $ident($consvis $ty); impl $ident { $(#[allow(non_upper_case_globals)] $vis const $variant: $ident = $ident($num);)* } @@ -18,8 +18,8 @@ macro_rules! enum_with_val { impl ::core::fmt::Debug for $ident { fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { match self { - $(&$ident::$variant => f.write_str(stringify!($ident)),)* - &$ident(v) => write!(f, "UNKNOWN({})", v), + $(&$ident::$variant => write!(f, "{}::{}", stringify!($ident), stringify!($variant)),)* + &$ident(v) => write!(f, "{}::UNKNOWN({})", stringify!($ident), v), } } } @@ -104,6 +104,11 @@ macro_rules! impl_debug_deserialize_serialize_hexstring { } } +pub fn align_up + BitAnd + Copy>(addr: T, align: T) -> T +{ + align_down(addr + (align - T::one()), align) +} + pub fn align_down + BitAnd + Copy>(addr: T, align: T) -> T { addr & !(align - T::one()) @@ -138,6 +143,11 @@ impl ReadRange { pub fn pos_in_stream(&self) -> u64 { self.start_from + self.inner_pos } + + /// Very dangerous. Don't use. + pub fn as_inner_mut(&mut self) -> &mut R { + &mut self.inner + } } impl io::Read for ReadRange {