mirror of
https://github.com/jakcron/nstool
synced 2024-11-22 05:29:30 +00:00
Merge pull request #112 from jakcron/feature-106-multipletickets
Add support for specifying multiple tickets / title keys
This commit is contained in:
commit
8798776405
6 changed files with 143 additions and 33 deletions
|
@ -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
|
||||||
|
```
|
|
@ -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;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in a new issue