Merge pull request #112 from jakcron/feature-106-multipletickets

Add support for specifying multiple tickets / title keys
This commit is contained in:
Jack 2024-01-20 14:01:37 +08:00 committed by GitHub
commit 8798776405
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 143 additions and 33 deletions

View file

@ -203,4 +203,18 @@ nstool <32 char rightsid>.tik
##### Personalised Tickets ##### 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. 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 # 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
```

View file

@ -12,19 +12,24 @@
#include <pietendo/hac/es/CertificateBody.h> #include <pietendo/hac/es/CertificateBody.h>
#include <pietendo/hac/es/TicketBody_V2.h> #include <pietendo/hac/es/TicketBody_V2.h>
nstool::KeyBagInitializer::KeyBagInitializer(bool isDev, const tc::Optional<tc::io::Path>& keyfile_path, const tc::Optional<tc::io::Path>& tik_path, const tc::Optional<tc::io::Path>& cert_path) nstool::KeyBagInitializer::KeyBagInitializer(bool isDev, const tc::Optional<tc::io::Path>& keyfile_path, const tc::Optional<tc::io::Path>& titlekeyfile_path, const std::vector<tc::io::Path>& tik_path_list, const tc::Optional<tc::io::Path>& cert_path)
{ {
if (keyfile_path.isSet()) if (keyfile_path.isSet())
{ {
importBaseKeyFile(keyfile_path.get(), isDev); importBaseKeyFile(keyfile_path.get(), isDev);
} }
if (titlekeyfile_path.isSet())
{
importTitleKeyFile(titlekeyfile_path.get());
}
if (cert_path.isSet()) if (cert_path.isSet())
{ {
importCertificateChain(cert_path.get()); 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. // this will populate known keys if they aren't supplied by the user provided keyfiles.
@ -447,7 +452,41 @@ void nstool::KeyBagInitializer::importBaseKeyFile(const tc::io::Path& keyfile_pa
void nstool::KeyBagInitializer::importTitleKeyFile(const tc::io::Path& keyfile_path) void nstool::KeyBagInitializer::importTitleKeyFile(const tc::io::Path& keyfile_path)
{ {
std::shared_ptr<tc::io::FileStream> keyfile_stream = std::make_shared<tc::io::FileStream>(tc::io::FileStream(keyfile_path, tc::io::FileMode::Open, tc::io::FileAccess::Read));
// import keyfile into a dictionary
std::map<std::string, std::string> 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())
{
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());
// parse the title key
tmp = tc::cli::FormatUtil::hexStringToBytes(itr->second);
if (tmp.size() != title_key_tmp.size())
{
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());
// 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) void nstool::KeyBagInitializer::importCertificateChain(const tc::io::Path& cert_path)
@ -551,10 +590,7 @@ void nstool::KeyBagInitializer::importTicket(const tc::io::Path& tik_path)
memcpy(enc_title_key.data(), tik.getBody().getEncTitleKey(), enc_title_key.size()); 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 // 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()) external_enc_content_keys[rights_id] = enc_title_key;
{
fallback_enc_content_key = enc_title_key;
}
// determine key to decrypt title key // determine key to decrypt title key
byte_t common_key_index = tik.getBody().getCommonKeyId(); byte_t common_key_index = tik.getBody().getCommonKeyId();
@ -581,7 +617,7 @@ void nstool::KeyBagInitializer::importTicket(const tc::io::Path& tik_path)
aes128_key_t dec_title_key; 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)); 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; external_content_keys[rights_id] = dec_title_key;
} }

View file

@ -39,6 +39,7 @@ struct KeyBag
// external content keys (nca<->ticket) // external content keys (nca<->ticket)
std::map<rights_id_t, aes128_key_t> external_content_keys; std::map<rights_id_t, aes128_key_t> external_content_keys;
std::map<rights_id_t, aes128_key_t> 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<aes128_key_t> 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<aes128_key_t> 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<aes128_key_t> fallback_content_key; // content key to be used when external_content_keys does not have the required content key (usually already decrypted from ticket) tc::Optional<aes128_key_t> 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 class KeyBagInitializer : public KeyBag
{ {
public: public:
KeyBagInitializer(bool isDev, const tc::Optional<tc::io::Path>& keyfile_path, const tc::Optional<tc::io::Path>& tik_path, const tc::Optional<tc::io::Path>& cert_path); KeyBagInitializer(bool isDev, const tc::Optional<tc::io::Path>& keyfile_path, const tc::Optional<tc::io::Path>& titlekeyfile_path, const std::vector<tc::io::Path>& tik_path_list, const tc::Optional<tc::io::Path>& cert_path);
private: private:
KeyBagInitializer(); KeyBagInitializer();

View file

@ -176,6 +176,15 @@ void nstool::NcaProcess::generateNcaBodyEncryptionKeys()
{ {
mContentKey.aes_ctr = mKeyCfg.fallback_content_key.get(); 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()) else if (mKeyCfg.fallback_enc_content_key.isSet())
{ {
tmp_key = mKeyCfg.fallback_enc_content_key.get(); tmp_key = mKeyCfg.fallback_enc_content_key.get();

View file

@ -260,6 +260,40 @@ private:
std::vector<std::string> mOptRegex; std::vector<std::string> mOptRegex;
}; };
class SingleParamPathArrayOptionHandler : public tc::cli::OptionParser::IOptionHandler
{
public:
SingleParamPathArrayOptionHandler(std::vector<tc::io::Path>& param, const std::vector<std::string>& opts) :
mParam(param),
mOptStrings(opts),
mOptRegex()
{}
const std::vector<std::string>& getOptionStrings() const
{
return mOptStrings;
}
const std::vector<std::string>& getOptionRegexPatterns() const
{
return mOptRegex;
}
void processOption(const std::string& option, const std::vector<std::string>& params)
{
if (params.size() != 1)
{
throw tc::ArgumentOutOfRangeException(fmt::format("Option \"{:s}\" requires a parameter.", option));
}
mParam.push_back(params[0]);
}
private:
std::vector<tc::io::Path>& mParam;
std::vector<std::string> mOptStrings;
std::vector<std::string> mOptRegex;
};
class FileTypeOptionHandler : public tc::cli::OptionParser::IOptionHandler class FileTypeOptionHandler : public tc::cli::OptionParser::IOptionHandler
{ {
public: public:
@ -504,7 +538,7 @@ nstool::SettingsInitializer::SettingsInitializer(const std::vector<std::string>&
mVerbose(false), mVerbose(false),
mNcaEncryptedContentKey(), mNcaEncryptedContentKey(),
mNcaContentKey(), mNcaContentKey(),
mTikPath(), mTikPathList(),
mCertPath() mCertPath()
{ {
// parse input arguments // parse input arguments
@ -532,29 +566,16 @@ nstool::SettingsInitializer::SettingsInitializer(const std::vector<std::string>&
// locate key file, if not specfied // locate key file, if not specfied
if (mKeysetPath.isNull()) if (mKeysetPath.isNull())
{ {
std::string home_path_str; loadKeyFile(mKeysetPath, opt.is_dev ? "dev.keys" : "prod.keys", "Maybe specify it with \"-k <path>\"?\n");
if (tc::os::getEnvVar("HOME", home_path_str) || tc::os::getEnvVar("USERPROFILE", home_path_str)) }
{ // locate title key file, if not specfied
tc::io::Path keyfile_path = tc::io::Path(home_path_str); if (mTitleKeysetPath.isNull())
keyfile_path.push_back(".switch"); {
keyfile_path.push_back(opt.is_dev ? "dev.keys" : "prod.keys"); loadKeyFile(mTitleKeysetPath, "title.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 <path>\"?\n", opt.is_dev ? "dev.keys" : "prod.keys");
}
}
else {
fmt::print("[WARNING] Failed to located \"{}\" keyfile. Maybe specify it with \"-k <path>\"?\n", opt.is_dev ? "dev.keys" : "prod.keys");
}
} }
// generate keybag // 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_enc_content_key = mNcaEncryptedContentKey;
opt.keybag.fallback_content_key = mNcaContentKey; opt.keybag.fallback_content_key = mNcaContentKey;
@ -618,9 +639,10 @@ void nstool::SettingsInitializer::parse_args(const std::vector<std::string>& arg
// get user-provided keydata // get user-provided keydata
opts.registerOptionHandler(std::shared_ptr<SingleParamPathOptionHandler>(new SingleParamPathOptionHandler(mKeysetPath, {"-k", "--keyset"}))); opts.registerOptionHandler(std::shared_ptr<SingleParamPathOptionHandler>(new SingleParamPathOptionHandler(mKeysetPath, {"-k", "--keyset"})));
//opts.registerOptionHandler(std::shared_ptr<SingleParamPathOptionHandler>(new SingleParamPathOptionHandler(mTitleKeysetPath, {"--titlekeyset"})));
opts.registerOptionHandler(std::shared_ptr<SingleParamAesKeyOptionHandler>(new SingleParamAesKeyOptionHandler(mNcaEncryptedContentKey, {"--titlekey"}))); opts.registerOptionHandler(std::shared_ptr<SingleParamAesKeyOptionHandler>(new SingleParamAesKeyOptionHandler(mNcaEncryptedContentKey, {"--titlekey"})));
opts.registerOptionHandler(std::shared_ptr<SingleParamAesKeyOptionHandler>(new SingleParamAesKeyOptionHandler(mNcaContentKey, {"--contentkey", "--bodykey"}))); opts.registerOptionHandler(std::shared_ptr<SingleParamAesKeyOptionHandler>(new SingleParamAesKeyOptionHandler(mNcaContentKey, {"--contentkey", "--bodykey"})));
opts.registerOptionHandler(std::shared_ptr<SingleParamPathOptionHandler>(new SingleParamPathOptionHandler(mTikPath, {"--tik"}))); opts.registerOptionHandler(std::shared_ptr<SingleParamPathArrayOptionHandler>(new SingleParamPathArrayOptionHandler(mTikPathList, {"--tik"})));
opts.registerOptionHandler(std::shared_ptr<SingleParamPathOptionHandler>(new SingleParamPathOptionHandler(mCertPath, {"--cert"}))); opts.registerOptionHandler(std::shared_ptr<SingleParamPathOptionHandler>(new SingleParamPathOptionHandler(mCertPath, {"--cert"})));
// code options // 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<tc::io::Path>& 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 bool nstool::SettingsInitializer::determineValidNcaFromSample(const tc::ByteData& sample) const
{ {

View file

@ -136,11 +136,15 @@ private:
bool mVerbose; bool mVerbose;
tc::Optional<tc::io::Path> mKeysetPath; tc::Optional<tc::io::Path> mKeysetPath;
tc::Optional<tc::io::Path> mTitleKeysetPath;
tc::Optional<KeyBag::aes128_key_t> mNcaEncryptedContentKey; tc::Optional<KeyBag::aes128_key_t> mNcaEncryptedContentKey;
tc::Optional<KeyBag::aes128_key_t> mNcaContentKey; tc::Optional<KeyBag::aes128_key_t> mNcaContentKey;
tc::Optional<tc::io::Path> mTikPath; std::vector<tc::io::Path> mTikPathList;
//tc::Optional<tc::io::Path> mTikPath;
tc::Optional<tc::io::Path> mCertPath; tc::Optional<tc::io::Path> mCertPath;
void loadKeyFile(tc::Optional<tc::io::Path>& keyfile_path, const std::string& keyfile_name, const std::string& cli_hint);
bool determineValidNcaFromSample(const tc::ByteData& raw_data) const; bool determineValidNcaFromSample(const tc::ByteData& raw_data) const;
bool determineValidEsCertFromSample(const tc::ByteData& raw_data) const; bool determineValidEsCertFromSample(const tc::ByteData& raw_data) const;
bool determineValidEsTikFromSample(const tc::ByteData& raw_data) const; bool determineValidEsTikFromSample(const tc::ByteData& raw_data) const;