From e205cb6a4f28f87a12732a0175941cb45e86e748 Mon Sep 17 00:00:00 2001 From: Jack Date: Mon, 1 Jan 2024 13:22:39 +0800 Subject: [PATCH] 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 @@ private: 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 @@ private: 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;