From e205cb6a4f28f87a12732a0175941cb45e86e748 Mon Sep 17 00:00:00 2001 From: Jack Date: Mon, 1 Jan 2024 13:22:39 +0800 Subject: [PATCH 1/5] Change --tik so that it can be invoked multiple times to specify a list of ticket files. Additionally added support for hactool title.keys --- src/KeyBag.cpp | 50 +++++++++++++++++++++----- src/KeyBag.h | 3 +- src/NcaProcess.cpp | 9 +++++ src/Settings.cpp | 90 ++++++++++++++++++++++++++++++++++------------ src/Settings.h | 6 +++- 5 files changed, 126 insertions(+), 32 deletions(-) diff --git a/src/KeyBag.cpp b/src/KeyBag.cpp index 992eeda..b40bbf2 100644 --- a/src/KeyBag.cpp +++ b/src/KeyBag.cpp @@ -12,19 +12,24 @@ #include #include -nstool::KeyBagInitializer::KeyBagInitializer(bool isDev, const tc::Optional& keyfile_path, const tc::Optional& tik_path, const tc::Optional& cert_path) +nstool::KeyBagInitializer::KeyBagInitializer(bool isDev, const tc::Optional& keyfile_path, const tc::Optional& titlekeyfile_path, const std::vector& tik_path_list, const tc::Optional& cert_path) { if (keyfile_path.isSet()) { importBaseKeyFile(keyfile_path.get(), isDev); } + if (titlekeyfile_path.isSet()) + { + importTitleKeyFile(titlekeyfile_path.get()); + } if (cert_path.isSet()) { importCertificateChain(cert_path.get()); } - if (tik_path.isSet()) + if (!tik_path_list.empty()) { - importTicket(tik_path.get()); + for (auto itr = tik_path_list.begin(); itr != tik_path_list.end(); itr++) + importTicket(*itr); } // this will populate known keys if they aren't supplied by the user provided keyfiles. @@ -447,7 +452,39 @@ void nstool::KeyBagInitializer::importBaseKeyFile(const tc::io::Path& keyfile_pa void nstool::KeyBagInitializer::importTitleKeyFile(const tc::io::Path& keyfile_path) { + std::shared_ptr keyfile_stream = std::make_shared(tc::io::FileStream(keyfile_path, tc::io::FileMode::Open, tc::io::FileAccess::Read)); + + // import keyfile into a dictionary + std::map keyfile_dict; + processResFile(keyfile_stream, keyfile_dict); + + // process title keys + tc::ByteData tmp; + KeyBag::rights_id_t rights_id_tmp; + KeyBag::aes128_key_t title_key_tmp; + for (auto itr = keyfile_dict.begin(); itr != keyfile_dict.end(); itr++) + { + //fmt::print("RightsID[{:s}] = TitleKey[{:s}]\n", itr->first, itr->second); + // parse the rights id + tmp = tc::cli::FormatUtil::hexStringToBytes(itr->first); + if (tmp.size() != rights_id_tmp.size()) + { + throw tc::ArgumentException("nstool::KeyBagInitializer", "RightsID: \"" + itr->first + "\" has incorrect length"); + } + memcpy(rights_id_tmp.data(), tmp.data(), rights_id_tmp.size()); + + // parse the title key + tmp = tc::cli::FormatUtil::hexStringToBytes(itr->second); + if (tmp.size() != title_key_tmp.size()) + { + throw tc::ArgumentException("nstool::KeyBagInitializer", "TitleKey for \""+ itr->first + "\": \"" + itr->second + "\" has incorrect length"); + } + memcpy(title_key_tmp.data(), tmp.data(), title_key_tmp.size()); + + // save to encrypted key dict + external_enc_content_keys[rights_id_tmp] = title_key_tmp; + } } void nstool::KeyBagInitializer::importCertificateChain(const tc::io::Path& cert_path) @@ -551,10 +588,7 @@ void nstool::KeyBagInitializer::importTicket(const tc::io::Path& tik_path) memcpy(enc_title_key.data(), tik.getBody().getEncTitleKey(), enc_title_key.size()); // save the encrypted title key as the fallback enc content key incase the ticket was malformed and workarounds to decrypt it in isolation fail - if (fallback_enc_content_key.isNull()) - { - fallback_enc_content_key = enc_title_key; - } + external_enc_content_keys[rights_id] = enc_title_key; // determine key to decrypt title key byte_t common_key_index = tik.getBody().getCommonKeyId(); @@ -581,7 +615,7 @@ void nstool::KeyBagInitializer::importTicket(const tc::io::Path& tik_path) aes128_key_t dec_title_key; tc::crypto::DecryptAes128Ecb(dec_title_key.data(), enc_title_key.data(), sizeof(aes128_key_t), etik_common_key[common_key_index].data(), sizeof(aes128_key_t)); - // add to key dict + // add to decrypted key dict external_content_keys[rights_id] = dec_title_key; } diff --git a/src/KeyBag.h b/src/KeyBag.h index a29baa1..b6325f4 100644 --- a/src/KeyBag.h +++ b/src/KeyBag.h @@ -39,6 +39,7 @@ struct KeyBag // external content keys (nca<->ticket) std::map external_content_keys; + std::map external_enc_content_keys; // encrypted content key list to be used when external_content_keys does not have the required content key (usually taken raw from ticket) tc::Optional fallback_enc_content_key; // encrypted content key to be used when external_content_keys does not have the required content key (usually taken raw from ticket) tc::Optional fallback_content_key; // content key to be used when external_content_keys does not have the required content key (usually already decrypted from ticket) @@ -70,7 +71,7 @@ struct KeyBag class KeyBagInitializer : public KeyBag { public: - KeyBagInitializer(bool isDev, const tc::Optional& keyfile_path, const tc::Optional& tik_path, const tc::Optional& cert_path); + KeyBagInitializer(bool isDev, const tc::Optional& keyfile_path, const tc::Optional& titlekeyfile_path, const std::vector& tik_path_list, const tc::Optional& cert_path); private: KeyBagInitializer(); diff --git a/src/NcaProcess.cpp b/src/NcaProcess.cpp index 5a60744..1c0dcfb 100644 --- a/src/NcaProcess.cpp +++ b/src/NcaProcess.cpp @@ -176,6 +176,15 @@ void nstool::NcaProcess::generateNcaBodyEncryptionKeys() { mContentKey.aes_ctr = mKeyCfg.fallback_content_key.get(); } + else if (mKeyCfg.external_enc_content_keys.find(mHdr.getRightsId()) != mKeyCfg.external_enc_content_keys.end()) + { + tmp_key = mKeyCfg.external_enc_content_keys[mHdr.getRightsId()]; + if (mKeyCfg.etik_common_key.find(masterkey_rev) != mKeyCfg.etik_common_key.end()) + { + pie::hac::AesKeygen::generateKey(tmp_key.data(), tmp_key.data(), mKeyCfg.etik_common_key[masterkey_rev].data()); + mContentKey.aes_ctr = tmp_key; + } + } else if (mKeyCfg.fallback_enc_content_key.isSet()) { tmp_key = mKeyCfg.fallback_enc_content_key.get(); diff --git a/src/Settings.cpp b/src/Settings.cpp index bf615f2..46d5d13 100644 --- a/src/Settings.cpp +++ b/src/Settings.cpp @@ -260,6 +260,40 @@ class SingleParamAesKeyOptionHandler : public tc::cli::OptionParser::IOptionHand std::vector mOptRegex; }; +class SingleParamPathArrayOptionHandler : public tc::cli::OptionParser::IOptionHandler +{ +public: + SingleParamPathArrayOptionHandler(std::vector& param, const std::vector& opts) : + mParam(param), + mOptStrings(opts), + mOptRegex() + {} + + const std::vector& getOptionStrings() const + { + return mOptStrings; + } + + const std::vector& getOptionRegexPatterns() const + { + return mOptRegex; + } + + void processOption(const std::string& option, const std::vector& params) + { + if (params.size() != 1) + { + throw tc::ArgumentOutOfRangeException(fmt::format("Option \"{:s}\" requires a parameter.", option)); + } + + mParam.push_back(params[0]); + } +private: + std::vector& mParam; + std::vector mOptStrings; + std::vector mOptRegex; +}; + class FileTypeOptionHandler : public tc::cli::OptionParser::IOptionHandler { public: @@ -504,7 +538,7 @@ nstool::SettingsInitializer::SettingsInitializer(const std::vector& mVerbose(false), mNcaEncryptedContentKey(), mNcaContentKey(), - mTikPath(), + mTikPathList(), mCertPath() { // parse input arguments @@ -532,29 +566,16 @@ nstool::SettingsInitializer::SettingsInitializer(const std::vector& // locate key file, if not specfied if (mKeysetPath.isNull()) { - std::string home_path_str; - if (tc::os::getEnvVar("HOME", home_path_str) || tc::os::getEnvVar("USERPROFILE", home_path_str)) - { - tc::io::Path keyfile_path = tc::io::Path(home_path_str); - keyfile_path.push_back(".switch"); - keyfile_path.push_back(opt.is_dev ? "dev.keys" : "prod.keys"); - - try { - tc::io::FileStream test = tc::io::FileStream(keyfile_path, tc::io::FileMode::Open, tc::io::FileAccess::Read); - - mKeysetPath = keyfile_path; - } - catch (tc::io::FileNotFoundException&) { - fmt::print("[WARNING] Failed to load \"{}\" keyfile. Maybe specify it with \"-k \"?\n", opt.is_dev ? "dev.keys" : "prod.keys"); - } - } - else { - fmt::print("[WARNING] Failed to located \"{}\" keyfile. Maybe specify it with \"-k \"?\n", opt.is_dev ? "dev.keys" : "prod.keys"); - } + loadKeyFile(mKeysetPath, opt.is_dev ? "dev.keys" : "prod.keys", "Maybe specify it with \"-k \"?\n"); + } + // locate title key file, if not specfied + if (mTitleKeysetPath.isNull()) + { + loadKeyFile(mTitleKeysetPath, "title.keys", ""); } // generate keybag - opt.keybag = KeyBagInitializer(opt.is_dev, mKeysetPath, mTikPath, mCertPath); + opt.keybag = KeyBagInitializer(opt.is_dev, mKeysetPath, mTitleKeysetPath, mTikPathList, mCertPath); opt.keybag.fallback_enc_content_key = mNcaEncryptedContentKey; opt.keybag.fallback_content_key = mNcaContentKey; @@ -618,9 +639,10 @@ void nstool::SettingsInitializer::parse_args(const std::vector& arg // get user-provided keydata opts.registerOptionHandler(std::shared_ptr(new SingleParamPathOptionHandler(mKeysetPath, {"-k", "--keyset"}))); + //opts.registerOptionHandler(std::shared_ptr(new SingleParamPathOptionHandler(mTitleKeysetPath, {"--titlekeyset"}))); opts.registerOptionHandler(std::shared_ptr(new SingleParamAesKeyOptionHandler(mNcaEncryptedContentKey, {"--titlekey"}))); opts.registerOptionHandler(std::shared_ptr(new SingleParamAesKeyOptionHandler(mNcaContentKey, {"--contentkey", "--bodykey"}))); - opts.registerOptionHandler(std::shared_ptr(new SingleParamPathOptionHandler(mTikPath, {"--tik"}))); + opts.registerOptionHandler(std::shared_ptr(new SingleParamPathArrayOptionHandler(mTikPathList, {"--tik"}))); opts.registerOptionHandler(std::shared_ptr(new SingleParamPathOptionHandler(mCertPath, {"--cert"}))); // code options @@ -967,6 +989,30 @@ void nstool::SettingsInitializer::dump_rsa_key(const KeyBag::rsa_key_t& key, con } } +void nstool::SettingsInitializer::loadKeyFile(tc::Optional& keyfile_path, const std::string& keyfile_name, const std::string& cli_hint) +{ + std::string home_path_str; + if (tc::os::getEnvVar("HOME", home_path_str) || tc::os::getEnvVar("USERPROFILE", home_path_str)) + { + tc::io::Path tmp_path = tc::io::Path(home_path_str); + tmp_path.push_back(".switch"); + tmp_path.push_back(keyfile_name); + + try { + tc::io::FileStream test = tc::io::FileStream(tmp_path, tc::io::FileMode::Open, tc::io::FileAccess::Read); + + keyfile_path = tmp_path; + } + catch (tc::io::FileNotFoundException&) { + fmt::print("[WARNING] Failed to load \"{}\" keyfile.{}\n", keyfile_name, cli_hint); + } + } + else { + fmt::print("[WARNING] Failed to locate \"{}\" keyfile.{}\n", keyfile_name, cli_hint); + } + +} + bool nstool::SettingsInitializer::determineValidNcaFromSample(const tc::ByteData& sample) const { diff --git a/src/Settings.h b/src/Settings.h index ee1246a..28dc0eb 100644 --- a/src/Settings.h +++ b/src/Settings.h @@ -136,11 +136,15 @@ class SettingsInitializer : public Settings bool mVerbose; tc::Optional mKeysetPath; + tc::Optional mTitleKeysetPath; tc::Optional mNcaEncryptedContentKey; tc::Optional mNcaContentKey; - tc::Optional mTikPath; + std::vector mTikPathList; + //tc::Optional mTikPath; tc::Optional mCertPath; + void loadKeyFile(tc::Optional& keyfile_path, const std::string& keyfile_name, const std::string& cli_hint); + bool determineValidNcaFromSample(const tc::ByteData& raw_data) const; bool determineValidEsCertFromSample(const tc::ByteData& raw_data) const; bool determineValidEsTikFromSample(const tc::ByteData& raw_data) const; From 1d680c2098ec751b16ac2bcc6c566f1c2bfa5921 Mon Sep 17 00:00:00 2001 From: Jack Date: Sat, 20 Jan 2024 11:16:25 +0800 Subject: [PATCH 2/5] Update SWITCH_KEYS.md to indicate title.keys is now supported. --- SWITCH_KEYS.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/SWITCH_KEYS.md b/SWITCH_KEYS.md index 1834679..70ea97a 100644 --- a/SWITCH_KEYS.md +++ b/SWITCH_KEYS.md @@ -203,4 +203,18 @@ nstool <32 char rightsid>.tik ##### Personalised Tickets If the ticket is personalised (encrypted with console unique RSA key), NSTool will not support it. You will need to use extract the title key with another tool and pass the encrypted title key directly with the `--titlekey` option. -# Title \ No newline at end of file +# Title Keys (title.keys) +In order for NSTool to decrypt NCA files that use external content keys, the ticket or key data be provided to NSTool. For convience NSTool supports the hactool `title.keys` format. This file can store a dictionary of title keys, so that specifying a ticket or key data manually is not required, provided it is present in `title.keys`. This file must be present in: ___$HOME/.switch/___ . + +## Format +* This file is in the format of (rights_id, title_key) pairs, each on their own line. +* There is no limit on the number of pairs. +* The `;` is the comment indicator. When parsing a file, it is treated as a new line character. +* The format is case insensitive + + +### Example +For example if rights id `010003000e1468000000000000000008` had a title key `8fa820b219781d331cca08968e6e5b52`, the row would look like this: +``` +010003000e1468000000000000000008 = 8fa820b219781d331cca08968e6e5b52 +``` \ No newline at end of file From 811334bae5fe610852ef178347f67eae4e40df2a Mon Sep 17 00:00:00 2001 From: Jack Date: Sat, 20 Jan 2024 11:17:04 +0800 Subject: [PATCH 3/5] Misc --- SWITCH_KEYS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SWITCH_KEYS.md b/SWITCH_KEYS.md index 70ea97a..a8b7cae 100644 --- a/SWITCH_KEYS.md +++ b/SWITCH_KEYS.md @@ -207,7 +207,7 @@ If the ticket is personalised (encrypted with console unique RSA key), NSTool wi In order for NSTool to decrypt NCA files that use external content keys, the ticket or key data be provided to NSTool. For convience NSTool supports the hactool `title.keys` format. This file can store a dictionary of title keys, so that specifying a ticket or key data manually is not required, provided it is present in `title.keys`. This file must be present in: ___$HOME/.switch/___ . ## Format -* This file is in the format of (rights_id, title_key) pairs, each on their own line. +* This file is in the format of (rights_id = title_key) pairs, each on their own line. * There is no limit on the number of pairs. * The `;` is the comment indicator. When parsing a file, it is treated as a new line character. * The format is case insensitive From 85dd51f496780cea6f37490c07c95b811bc6f11d Mon Sep 17 00:00:00 2001 From: Jack Date: Sat, 20 Jan 2024 11:20:56 +0800 Subject: [PATCH 4/5] Skip bad title.keys rows instead of throwing an exception. --- src/KeyBag.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/KeyBag.cpp b/src/KeyBag.cpp index b40bbf2..11df8de 100644 --- a/src/KeyBag.cpp +++ b/src/KeyBag.cpp @@ -470,7 +470,8 @@ void nstool::KeyBagInitializer::importTitleKeyFile(const tc::io::Path& keyfile_p tmp = tc::cli::FormatUtil::hexStringToBytes(itr->first); if (tmp.size() != rights_id_tmp.size()) { - throw tc::ArgumentException("nstool::KeyBagInitializer", "RightsID: \"" + itr->first + "\" has incorrect length"); + fmt::print("[nstool::KeyBagInitializer WARNING] RightsID: \"{}\" has incorrect length. Skipping...\n", itr->first); + continue; } memcpy(rights_id_tmp.data(), tmp.data(), rights_id_tmp.size()); @@ -478,7 +479,8 @@ void nstool::KeyBagInitializer::importTitleKeyFile(const tc::io::Path& keyfile_p tmp = tc::cli::FormatUtil::hexStringToBytes(itr->second); if (tmp.size() != title_key_tmp.size()) { - throw tc::ArgumentException("nstool::KeyBagInitializer", "TitleKey for \""+ itr->first + "\": \"" + itr->second + "\" has incorrect length"); + fmt::print("[nstool::KeyBagInitializer WARNING] TitleKey for \"{}\": \"{}\" has incorrect length. Skipping...\n", itr->first, itr->second); + continue; } memcpy(title_key_tmp.data(), tmp.data(), title_key_tmp.size()); From b8464efa2752971e3c8492124f7b679420cc72a0 Mon Sep 17 00:00:00 2001 From: Jack Date: Sat, 20 Jan 2024 14:05:11 +0800 Subject: [PATCH 5/5] Set version to v1.9.0 --- src/version.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/version.h b/src/version.h index 1ff741e..b16c8a2 100644 --- a/src/version.h +++ b/src/version.h @@ -1,7 +1,7 @@ #pragma once #define APP_NAME "NSTool" #define BIN_NAME "nstool" -#define VER_MAJOR 0 -#define VER_MINOR 0 +#define VER_MAJOR 1 +#define VER_MINOR 9 #define VER_PATCH 0 #define AUTHORS "jakcron" \ No newline at end of file