mirror of
https://github.com/DarkStore-3DS/Project_CTR.git
synced 2026-07-02 16:59:03 +00:00
Add support for processing tik/tmd files directly.
This commit is contained in:
@@ -0,0 +1,367 @@
|
||||
#include "TikProcess.h"
|
||||
#include <tc/io.h>
|
||||
#include <tc/cli.h>
|
||||
#include <tc/crypto.h>
|
||||
#include <tc/ArgumentNullException.h>
|
||||
#include <tc/NotSupportedException.h>
|
||||
|
||||
ctrtool::TikProcess::TikProcess() :
|
||||
mModuleLabel("ctrtool::TikProcess"),
|
||||
mInputStream(),
|
||||
mKeyBag(),
|
||||
mShowInfo(false),
|
||||
mVerbose(false),
|
||||
mVerify(false),
|
||||
mIssuerSigner(),
|
||||
mCertImportedIssuerSigner(),
|
||||
mCertChain(),
|
||||
mCertSigValid(),
|
||||
mTicket(),
|
||||
mTicketSigValid(ValidState::Unchecked),
|
||||
mDecryptedTitleKey()
|
||||
{
|
||||
}
|
||||
|
||||
void ctrtool::TikProcess::setInputStream(const std::shared_ptr<tc::io::IStream>& input_stream)
|
||||
{
|
||||
mInputStream = input_stream;
|
||||
}
|
||||
|
||||
void ctrtool::TikProcess::setKeyBag(const ctrtool::KeyBag& key_bag)
|
||||
{
|
||||
mKeyBag = key_bag;
|
||||
}
|
||||
|
||||
void ctrtool::TikProcess::setCliOutputMode(bool show_info)
|
||||
{
|
||||
mShowInfo = show_info;
|
||||
}
|
||||
|
||||
void ctrtool::TikProcess::setVerboseMode(bool verbose)
|
||||
{
|
||||
mVerbose = verbose;
|
||||
}
|
||||
|
||||
void ctrtool::TikProcess::setVerifyMode(bool verify)
|
||||
{
|
||||
mVerify = verify;
|
||||
}
|
||||
|
||||
void ctrtool::TikProcess::process()
|
||||
{
|
||||
importIssuerProfiles();
|
||||
importData();
|
||||
|
||||
if (mVerify)
|
||||
{
|
||||
verifyData();
|
||||
}
|
||||
|
||||
if (mShowInfo)
|
||||
{
|
||||
printData();
|
||||
}
|
||||
}
|
||||
|
||||
void ctrtool::TikProcess::importIssuerProfiles()
|
||||
{
|
||||
// import issuer profiles from keybag
|
||||
for (auto itr = mKeyBag.broadon_rsa_signer.begin(); itr != mKeyBag.broadon_rsa_signer.end(); itr++)
|
||||
{
|
||||
brd::es::ESSigType sigType = itr->first == "Root" ? brd::es::ESSigType::RSA4096_SHA256 : brd::es::ESSigType::RSA2048_SHA256;
|
||||
mIssuerSigner.insert(std::pair<std::string, std::shared_ptr<ntd::n3ds::es::ISigner>>(itr->first, std::make_shared<ntd::n3ds::es::RsaSigner>(ntd::n3ds::es::RsaSigner(sigType, itr->first, itr->second.key))));
|
||||
}
|
||||
}
|
||||
|
||||
void ctrtool::TikProcess::importData()
|
||||
{
|
||||
// validate input stream
|
||||
if (mInputStream == nullptr)
|
||||
{
|
||||
throw tc::ArgumentNullException(mModuleLabel, "Input stream was null.");
|
||||
}
|
||||
if (mInputStream->canRead() == false || mInputStream->canSeek() == false)
|
||||
{
|
||||
throw tc::InvalidOperationException(mModuleLabel, "Input stream requires read/seek permissions.");
|
||||
}
|
||||
|
||||
// process ticket
|
||||
{
|
||||
mTicket = ntd::n3ds::es::TicketDeserialiser(mInputStream);
|
||||
mTicketSigValid = ValidState::Unchecked;
|
||||
|
||||
// determine title key
|
||||
if (mKeyBag.common_key.find(mTicket.key_id) != mKeyBag.common_key.end())
|
||||
{
|
||||
fmt::print("[LOG] Decrypting titlekey from ticket.\n");
|
||||
|
||||
// get common key
|
||||
auto common_key = mKeyBag.common_key[mTicket.key_id];
|
||||
|
||||
// initialise iv
|
||||
std::array<byte_t, 16> title_key_iv;
|
||||
memset(title_key_iv.data(), 0, title_key_iv.size());
|
||||
((tc::bn::be64<uint64_t>*)(&(title_key_iv[0])))->wrap(mTicket.title_id);
|
||||
|
||||
// decrypt title key
|
||||
std::array<byte_t, 16> title_key;
|
||||
tc::crypto::DecryptAes128Cbc(title_key.data(), mTicket.title_key.data(), title_key.size(), common_key.data(), common_key.size(), title_key_iv.data(), title_key_iv.size());
|
||||
|
||||
mDecryptedTitleKey = title_key;
|
||||
}
|
||||
else
|
||||
{
|
||||
fmt::print("[LOG] Cannot determine titlekey.\n");
|
||||
}
|
||||
}
|
||||
|
||||
// process trailing cert chain (this assumes ntd::n3ds::es::TicketDeserialiser leaves the stream position at the end of the ticket data)
|
||||
while (mInputStream->position() < mInputStream->length())
|
||||
{
|
||||
std::shared_ptr<tc::io::IStream> cert_stream = std::make_shared<tc::io::SubStream>(tc::io::SubStream(mInputStream, mInputStream->position(), mInputStream->length() - mInputStream->position()));
|
||||
mCertChain.push_back(ntd::n3ds::es::CertificateDeserialiser(cert_stream));
|
||||
mCertSigValid.push_back(ValidState::Unchecked);
|
||||
|
||||
// update position of input stream
|
||||
//mInputStream->seek(cert_stream->position(), tc::io::SeekOrigin::Current);
|
||||
|
||||
// import issuer profile from certificate
|
||||
if (mCertChain.back().public_key_type == brd::es::ESCertPubKeyType::RSA2048)
|
||||
{
|
||||
std::string issuer = fmt::format("{}-{}", mCertChain.back().signature.issuer, mCertChain.back().subject);
|
||||
brd::es::ESSigType sig_type = brd::es::ESSigType::RSA2048_SHA256;
|
||||
auto& public_key = mCertChain.back().rsa2048_public_key;
|
||||
|
||||
mCertImportedIssuerSigner.insert(std::pair<std::string, std::shared_ptr<ntd::n3ds::es::ISigner>>(issuer, std::make_shared<ntd::n3ds::es::RsaSigner>(ntd::n3ds::es::RsaSigner(sig_type, issuer, tc::crypto::RsaPublicKey(public_key.m.data(), public_key.m.size())))));
|
||||
}
|
||||
else if (mCertChain.back().public_key_type == brd::es::ESCertPubKeyType::RSA4096)
|
||||
{
|
||||
std::string issuer = fmt::format("{}-{}", mCertChain.back().signature.issuer + mCertChain.back().subject);
|
||||
brd::es::ESSigType sig_type = brd::es::ESSigType::RSA4096_SHA256;
|
||||
auto& public_key = mCertChain.back().rsa4096_public_key;
|
||||
|
||||
mCertImportedIssuerSigner.insert(std::pair<std::string, std::shared_ptr<ntd::n3ds::es::ISigner>>(issuer, std::make_shared<ntd::n3ds::es::RsaSigner>(ntd::n3ds::es::RsaSigner(sig_type, issuer, tc::crypto::RsaPublicKey(public_key.m.data(), public_key.m.size())))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ctrtool::TikProcess::verifyData()
|
||||
{
|
||||
// verify cert
|
||||
for (size_t i = 0; i < mCertChain.size(); i++)
|
||||
{
|
||||
auto keybag_issuer_itr = mIssuerSigner.find(mCertChain[i].signature.issuer);
|
||||
auto local_issuer_itr = mCertImportedIssuerSigner.find(mCertChain[i].signature.issuer);
|
||||
|
||||
// try first with the keybag imported issuer
|
||||
if (keybag_issuer_itr != mIssuerSigner.end() && keybag_issuer_itr->second->getSigType() == mCertChain[i].signature.sig_type)
|
||||
{
|
||||
mCertSigValid[i] = keybag_issuer_itr->second->verifyHash(mCertChain[i].calculated_hash.data(), mCertChain[i].signature.sig.data()) ? ValidState::Good : ValidState::Fail;
|
||||
}
|
||||
// fallback try with the issuer profiles imported from the local certificates
|
||||
else if (local_issuer_itr != mCertImportedIssuerSigner.end() && local_issuer_itr->second->getSigType() == mCertChain[i].signature.sig_type)
|
||||
{
|
||||
mCertSigValid[i] = local_issuer_itr->second->verifyHash(mCertChain[i].calculated_hash.data(), mCertChain[i].signature.sig.data()) ? ValidState::Good : ValidState::Fail;
|
||||
}
|
||||
else
|
||||
{
|
||||
// cannot locate rsa key to verify
|
||||
fmt::print(stderr, "Could not read public key for \"{}\" (certificate).\n", mCertChain[i].signature.issuer);
|
||||
mCertSigValid[i] = ValidState::Fail;
|
||||
}
|
||||
}
|
||||
|
||||
// verify ticket
|
||||
{
|
||||
auto keybag_issuer_itr = mIssuerSigner.find(mTicket.signature.issuer);
|
||||
auto local_issuer_itr = mCertImportedIssuerSigner.find(mTicket.signature.issuer);
|
||||
|
||||
// try first with the keybag imported issuer
|
||||
if (keybag_issuer_itr != mIssuerSigner.end() && keybag_issuer_itr->second->getSigType() == mTicket.signature.sig_type)
|
||||
{
|
||||
mTicketSigValid = keybag_issuer_itr->second->verifyHash(mTicket.calculated_hash.data(), mTicket.signature.sig.data()) ? ValidState::Good : ValidState::Fail;
|
||||
}
|
||||
// fallback try with the issuer profiles imported from the local certificates
|
||||
else if (local_issuer_itr != mCertImportedIssuerSigner.end() && local_issuer_itr->second->getSigType() == mTicket.signature.sig_type)
|
||||
{
|
||||
mTicketSigValid = local_issuer_itr->second->verifyHash(mTicket.calculated_hash.data(), mTicket.signature.sig.data()) ? ValidState::Good : ValidState::Fail;
|
||||
}
|
||||
else
|
||||
{
|
||||
// cannot locate rsa key to verify
|
||||
fmt::print(stderr, "Could not read public key for \"{}\" (ticket).\n", mTicket.signature.issuer);
|
||||
mTicketSigValid = ValidState::Fail;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ctrtool::TikProcess::printData()
|
||||
{
|
||||
{
|
||||
fmt::print("Ticket:\n");
|
||||
fmt::print("|- DigitalSignature: {:s} \n", getValidString(mTicketSigValid));
|
||||
fmt::print("| |- SigType: {:s} (0x{:x})\n", getSigTypeString(mTicket.signature.sig_type), (uint32_t)mTicket.signature.sig_type);
|
||||
fmt::print("| |- Issuer: {:s}\n", mTicket.signature.issuer);
|
||||
fmt::print("| \\- Signature: {:s}\n", getTruncatedBytesString(mTicket.signature.sig.data(), mTicket.signature.sig.size(), mVerbose));
|
||||
fmt::print("|- TitleKey: {}", tc::cli::FormatUtil::formatBytesAsString(mTicket.title_key.data(), mTicket.title_key.size(), true, ""));
|
||||
if (mDecryptedTitleKey.isSet())
|
||||
{
|
||||
fmt::print(" (decrypted: {})", tc::cli::FormatUtil::formatBytesAsString(mDecryptedTitleKey.get().data(), mDecryptedTitleKey.get().size(), true, ""));
|
||||
}
|
||||
fmt::print("\n");
|
||||
fmt::print("|- TicketId: {:016x}\n", mTicket.ticket_id);
|
||||
fmt::print("|- DeviceId: {:08x}\n", mTicket.device_id);
|
||||
fmt::print("|- TitleId: {:016x}\n", mTicket.title_id);
|
||||
fmt::print("|- TicketVersion: {} ({:d})\n", getTitleVersionString(mTicket.ticket_version), mTicket.ticket_version);
|
||||
fmt::print("|- LicenseType: {:02x}\n", mTicket.license_type);
|
||||
fmt::print("|- KeyId: {:02x}\n", mTicket.key_id);
|
||||
fmt::print("|- ECAccountID: {:08x}\n", mTicket.ec_account_id);
|
||||
fmt::print("|- DemoLaunchCnt: {:d}\n", mTicket.launch_count);
|
||||
fmt::print("\\- EnabledContent:\n");
|
||||
std::vector<size_t> enabled_content;
|
||||
for (size_t i = 0; i < mTicket.enabled_content.size(); i++)
|
||||
{
|
||||
if (mTicket.enabled_content.test(i))
|
||||
{
|
||||
enabled_content.push_back(i);
|
||||
}
|
||||
}
|
||||
for (size_t i = 0; i < enabled_content.size(); i++)
|
||||
{
|
||||
fmt::print(" {:1}- 0x{:04x}\n", (i+1 < enabled_content.size() ? "|" : "\\"), enabled_content[i]);
|
||||
}
|
||||
}
|
||||
if (mCertChain.size() > 0)
|
||||
{
|
||||
fmt::print("Certificate Chain:\n");
|
||||
for (size_t i = 0; i < mCertChain.size(); i++)
|
||||
{
|
||||
#define _CERT_FORMAT_MACRO(x,y) (i+1 < mCertChain.size() ? (x) : (y))
|
||||
|
||||
fmt::print("{:1}- Certificate {:d}:\n", _CERT_FORMAT_MACRO("|","\\"), i);
|
||||
fmt::print("{:1} |- DigitalSignature: {:s}\n", _CERT_FORMAT_MACRO("|"," "), getValidString(mCertSigValid[i]));
|
||||
fmt::print("{:1} | |- SigType: {:s} (0x{:x})\n", _CERT_FORMAT_MACRO("|"," "), getSigTypeString(mCertChain[i].signature.sig_type), (uint32_t)mCertChain[i].signature.sig_type);
|
||||
fmt::print("{:1} | |- Issuer: {:s}\n", _CERT_FORMAT_MACRO("|"," "), mCertChain[i].signature.issuer);
|
||||
fmt::print("{:1} | \\- Signature: {:s}\n", _CERT_FORMAT_MACRO("|"," "), getTruncatedBytesString(mCertChain[i].signature.sig.data(), mCertChain[i].signature.sig.size(), mVerbose));
|
||||
fmt::print("{:1} |- Subject: {:s}\n", _CERT_FORMAT_MACRO("|"," "), mCertChain[i].subject);
|
||||
//fmt::print("{:1} |- Date: {:d}\n", _CERT_FORMAT_MACRO("|"," "), mCertChain[i].date);
|
||||
fmt::print("{:1} \\- PublicKey: {:s} (0x{:x})\n", _CERT_FORMAT_MACRO("|"," "), getCertificatePublicKeyTypeString(mCertChain[i].public_key_type), (uint32_t)mCertChain[i].public_key_type);
|
||||
switch (mCertChain[i].public_key_type)
|
||||
{
|
||||
case brd::es::ESCertPubKeyType::RSA4096:
|
||||
fmt::print("{:1} |- m: {:s}\n", _CERT_FORMAT_MACRO("|"," "), getTruncatedBytesString(mCertChain[i].rsa4096_public_key.m.data(), mCertChain[i].rsa4096_public_key.m.size(), mVerbose));
|
||||
fmt::print("{:1} \\- e: {:s}\n", _CERT_FORMAT_MACRO("|"," "), getTruncatedBytesString(mCertChain[i].rsa4096_public_key.e.data(), mCertChain[i].rsa4096_public_key.e.size(), mVerbose));
|
||||
break;
|
||||
case brd::es::ESCertPubKeyType::RSA2048:
|
||||
fmt::print("{:1} |- m: {:s}\n", _CERT_FORMAT_MACRO("|"," "), getTruncatedBytesString(mCertChain[i].rsa2048_public_key.m.data(), mCertChain[i].rsa2048_public_key.m.size(), mVerbose));
|
||||
fmt::print("{:1} \\- e: {:s}\n", _CERT_FORMAT_MACRO("|"," "), getTruncatedBytesString(mCertChain[i].rsa2048_public_key.e.data(), mCertChain[i].rsa2048_public_key.e.size(), mVerbose));
|
||||
break;
|
||||
case brd::es::ESCertPubKeyType::ECC:
|
||||
fmt::print("{:1} |- x: {:s}\n", _CERT_FORMAT_MACRO("|"," "), getTruncatedBytesString(mCertChain[i].ecc233_public_key.x.data(), mCertChain[i].ecc233_public_key.x.size(), mVerbose));
|
||||
fmt::print("{:1} \\- y: {:s}\n", _CERT_FORMAT_MACRO("|"," "), getTruncatedBytesString(mCertChain[i].ecc233_public_key.y.data(), mCertChain[i].ecc233_public_key.y.size(), mVerbose));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
#undef _CERT_FORMAT_MACRO
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::string ctrtool::TikProcess::getValidString(byte_t validstate)
|
||||
{
|
||||
std::string ret_str;
|
||||
|
||||
switch (validstate)
|
||||
{
|
||||
case ValidState::Unchecked:
|
||||
ret_str = "";
|
||||
break;
|
||||
case ValidState::Good:
|
||||
ret_str = "(GOOD)";
|
||||
break;
|
||||
case ValidState::Fail:
|
||||
default:
|
||||
ret_str = "(FAIL)";
|
||||
break;
|
||||
}
|
||||
|
||||
return ret_str;
|
||||
}
|
||||
|
||||
std::string ctrtool::TikProcess::getTruncatedBytesString(const byte_t* data, size_t len, bool do_not_truncate)
|
||||
{
|
||||
if (data == nullptr) { return fmt::format(""); }
|
||||
|
||||
std::string str = "";
|
||||
|
||||
if (len <= 8 || do_not_truncate)
|
||||
{
|
||||
str = tc::cli::FormatUtil::formatBytesAsString(data, len, true, "");
|
||||
}
|
||||
else
|
||||
{
|
||||
str = fmt::format("{:02X}{:02X}{:02X}{:02X}...{:02X}{:02X}{:02X}{:02X}", data[0], data[1], data[2], data[3], data[len-4], data[len-3], data[len-2], data[len-1]);
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
std::string ctrtool::TikProcess::getSigTypeString(brd::es::ESSigType sig_type)
|
||||
{
|
||||
std::string ret_str;
|
||||
|
||||
switch (sig_type)
|
||||
{
|
||||
case brd::es::ESSigType::RSA4096_SHA1:
|
||||
ret_str = "RSA-4096-SHA1";
|
||||
break;
|
||||
case brd::es::ESSigType::RSA2048_SHA1:
|
||||
ret_str = "RSA-2048-SHA1";
|
||||
break;
|
||||
case brd::es::ESSigType::ECC_SHA1:
|
||||
ret_str = "ECDSA-233-SHA1";
|
||||
break;
|
||||
case brd::es::ESSigType::RSA4096_SHA256:
|
||||
ret_str = "RSA-4096-SHA256";
|
||||
break;
|
||||
case brd::es::ESSigType::RSA2048_SHA256:
|
||||
ret_str = "RSA-2048-SHA256";
|
||||
break;
|
||||
case brd::es::ESSigType::ECC_SHA256:
|
||||
ret_str = "ECDSA-233-SHA256";
|
||||
break;
|
||||
default:
|
||||
ret_str = fmt::format("0x{:x}", (uint32_t)sig_type);
|
||||
}
|
||||
|
||||
return ret_str;
|
||||
}
|
||||
|
||||
std::string ctrtool::TikProcess::getCertificatePublicKeyTypeString(brd::es::ESCertPubKeyType public_key_type)
|
||||
{
|
||||
std::string ret_str;
|
||||
|
||||
switch (public_key_type)
|
||||
{
|
||||
case brd::es::ESCertPubKeyType::RSA4096:
|
||||
ret_str = "RSA-4096";
|
||||
break;
|
||||
case brd::es::ESCertPubKeyType::RSA2048:
|
||||
ret_str = "RSA-2048";
|
||||
break;
|
||||
case brd::es::ESCertPubKeyType::ECC:
|
||||
ret_str = "ECC-233";
|
||||
break;
|
||||
default:
|
||||
ret_str = fmt::format("0x{:x}", (uint32_t)public_key_type);
|
||||
}
|
||||
|
||||
return ret_str;
|
||||
}
|
||||
|
||||
std::string ctrtool::TikProcess::getTitleVersionString(uint16_t version)
|
||||
{
|
||||
return fmt::format("{major:d}.{minor:d}.{build:d}", fmt::arg("major", (uint32_t)((version >> 10) & 0x3F)), fmt::arg("minor", (uint32_t)((version >> 4) & 0x3F)), fmt::arg("build", (uint32_t)(version & 0xF)));
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
#include "types.h"
|
||||
#include "KeyBag.h"
|
||||
#include <tc/Optional.h>
|
||||
|
||||
#include <ntd/n3ds/es/RsaSigner.h>
|
||||
#include <ntd/n3ds/es/Certificate.h>
|
||||
#include <ntd/n3ds/es/Ticket.h>
|
||||
|
||||
namespace ctrtool {
|
||||
|
||||
class TikProcess
|
||||
{
|
||||
public:
|
||||
TikProcess();
|
||||
|
||||
void setInputStream(const std::shared_ptr<tc::io::IStream>& input_stream);
|
||||
void setKeyBag(const ctrtool::KeyBag& key_bag);
|
||||
void setCliOutputMode(bool show_header_info);
|
||||
void setVerboseMode(bool verbose);
|
||||
void setVerifyMode(bool verify);
|
||||
|
||||
void process();
|
||||
private:
|
||||
std::string mModuleLabel;
|
||||
|
||||
// input args
|
||||
std::shared_ptr<tc::io::IStream> mInputStream;
|
||||
ctrtool::KeyBag mKeyBag;
|
||||
bool mShowInfo;
|
||||
bool mVerbose;
|
||||
bool mVerify;
|
||||
|
||||
// process variables
|
||||
std::map<std::string, std::shared_ptr<ntd::n3ds::es::ISigner>> mIssuerSigner;
|
||||
std::map<std::string, std::shared_ptr<ntd::n3ds::es::ISigner>> mCertImportedIssuerSigner;
|
||||
|
||||
std::vector<ntd::n3ds::es::Certificate> mCertChain;
|
||||
std::vector<ValidState> mCertSigValid;
|
||||
|
||||
ntd::n3ds::es::Ticket mTicket;
|
||||
ValidState mTicketSigValid;
|
||||
|
||||
tc::Optional<KeyBag::Aes128Key> mDecryptedTitleKey;
|
||||
|
||||
// helper methods
|
||||
void importIssuerProfiles();
|
||||
void importData();
|
||||
void verifyData();
|
||||
void printData();
|
||||
|
||||
// string utils
|
||||
std::string getValidString(byte_t validstate);
|
||||
std::string getTruncatedBytesString(const byte_t* data, size_t len, bool do_not_truncate = false);
|
||||
std::string getSigTypeString(brd::es::ESSigType sig_type);
|
||||
std::string getCertificatePublicKeyTypeString(brd::es::ESCertPubKeyType public_key_type);
|
||||
std::string getTitleVersionString(uint16_t version);
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
#include "TmdProcess.h"
|
||||
#include <tc/io.h>
|
||||
#include <tc/cli.h>
|
||||
#include <tc/crypto.h>
|
||||
#include <tc/ArgumentNullException.h>
|
||||
#include <tc/NotSupportedException.h>
|
||||
|
||||
ctrtool::TmdProcess::TmdProcess() :
|
||||
mModuleLabel("ctrtool::TmdProcess"),
|
||||
mInputStream(),
|
||||
mKeyBag(),
|
||||
mShowInfo(false),
|
||||
mVerbose(false),
|
||||
mVerify(false),
|
||||
mIssuerSigner(),
|
||||
mCertImportedIssuerSigner(),
|
||||
mCertChain(),
|
||||
mCertSigValid(),
|
||||
mTitleMetaData(),
|
||||
mTitleMetaDataSigValid(ValidState::Unchecked)
|
||||
{
|
||||
}
|
||||
|
||||
void ctrtool::TmdProcess::setInputStream(const std::shared_ptr<tc::io::IStream>& input_stream)
|
||||
{
|
||||
mInputStream = input_stream;
|
||||
}
|
||||
|
||||
void ctrtool::TmdProcess::setKeyBag(const ctrtool::KeyBag& key_bag)
|
||||
{
|
||||
mKeyBag = key_bag;
|
||||
}
|
||||
|
||||
void ctrtool::TmdProcess::setCliOutputMode(bool show_info)
|
||||
{
|
||||
mShowInfo = show_info;
|
||||
}
|
||||
|
||||
void ctrtool::TmdProcess::setVerboseMode(bool verbose)
|
||||
{
|
||||
mVerbose = verbose;
|
||||
}
|
||||
|
||||
void ctrtool::TmdProcess::setVerifyMode(bool verify)
|
||||
{
|
||||
mVerify = verify;
|
||||
}
|
||||
|
||||
void ctrtool::TmdProcess::process()
|
||||
{
|
||||
importIssuerProfiles();
|
||||
importData();
|
||||
|
||||
if (mVerify)
|
||||
{
|
||||
verifyData();
|
||||
}
|
||||
|
||||
if (mShowInfo)
|
||||
{
|
||||
printData();
|
||||
}
|
||||
}
|
||||
|
||||
void ctrtool::TmdProcess::importIssuerProfiles()
|
||||
{
|
||||
// import issuer profiles from keybag
|
||||
for (auto itr = mKeyBag.broadon_rsa_signer.begin(); itr != mKeyBag.broadon_rsa_signer.end(); itr++)
|
||||
{
|
||||
brd::es::ESSigType sigType = itr->first == "Root" ? brd::es::ESSigType::RSA4096_SHA256 : brd::es::ESSigType::RSA2048_SHA256;
|
||||
mIssuerSigner.insert(std::pair<std::string, std::shared_ptr<ntd::n3ds::es::ISigner>>(itr->first, std::make_shared<ntd::n3ds::es::RsaSigner>(ntd::n3ds::es::RsaSigner(sigType, itr->first, itr->second.key))));
|
||||
}
|
||||
}
|
||||
|
||||
void ctrtool::TmdProcess::importData()
|
||||
{
|
||||
// validate input stream
|
||||
if (mInputStream == nullptr)
|
||||
{
|
||||
throw tc::ArgumentNullException(mModuleLabel, "Input stream was null.");
|
||||
}
|
||||
if (mInputStream->canRead() == false || mInputStream->canSeek() == false)
|
||||
{
|
||||
throw tc::InvalidOperationException(mModuleLabel, "Input stream requires read/seek permissions.");
|
||||
}
|
||||
|
||||
// process tmd
|
||||
{
|
||||
mTitleMetaData = ntd::n3ds::es::TitleMetaDataDeserialiser(mInputStream);
|
||||
mTitleMetaDataSigValid = ValidState::Unchecked;
|
||||
}
|
||||
|
||||
// process trailing cert chain (this assumes ntd::n3ds::es::TitleMetaDataDeserialiser leaves the stream position at the end of the tmd data)
|
||||
while (mInputStream->position() < mInputStream->length())
|
||||
{
|
||||
std::shared_ptr<tc::io::IStream> cert_stream = std::make_shared<tc::io::SubStream>(tc::io::SubStream(mInputStream, mInputStream->position(), mInputStream->length() - mInputStream->position()));
|
||||
mCertChain.push_back(ntd::n3ds::es::CertificateDeserialiser(cert_stream));
|
||||
mCertSigValid.push_back(ValidState::Unchecked);
|
||||
|
||||
// update position of input stream
|
||||
//mInputStream->seek(cert_stream->position(), tc::io::SeekOrigin::Current);
|
||||
|
||||
// import issuer profile from certificate
|
||||
if (mCertChain.back().public_key_type == brd::es::ESCertPubKeyType::RSA2048)
|
||||
{
|
||||
std::string issuer = fmt::format("{}-{}", mCertChain.back().signature.issuer, mCertChain.back().subject);
|
||||
brd::es::ESSigType sig_type = brd::es::ESSigType::RSA2048_SHA256;
|
||||
auto& public_key = mCertChain.back().rsa2048_public_key;
|
||||
|
||||
mCertImportedIssuerSigner.insert(std::pair<std::string, std::shared_ptr<ntd::n3ds::es::ISigner>>(issuer, std::make_shared<ntd::n3ds::es::RsaSigner>(ntd::n3ds::es::RsaSigner(sig_type, issuer, tc::crypto::RsaPublicKey(public_key.m.data(), public_key.m.size())))));
|
||||
}
|
||||
else if (mCertChain.back().public_key_type == brd::es::ESCertPubKeyType::RSA4096)
|
||||
{
|
||||
std::string issuer = fmt::format("{}-{}", mCertChain.back().signature.issuer + mCertChain.back().subject);
|
||||
brd::es::ESSigType sig_type = brd::es::ESSigType::RSA4096_SHA256;
|
||||
auto& public_key = mCertChain.back().rsa4096_public_key;
|
||||
|
||||
mCertImportedIssuerSigner.insert(std::pair<std::string, std::shared_ptr<ntd::n3ds::es::ISigner>>(issuer, std::make_shared<ntd::n3ds::es::RsaSigner>(ntd::n3ds::es::RsaSigner(sig_type, issuer, tc::crypto::RsaPublicKey(public_key.m.data(), public_key.m.size())))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ctrtool::TmdProcess::verifyData()
|
||||
{
|
||||
// verify cert
|
||||
for (size_t i = 0; i < mCertChain.size(); i++)
|
||||
{
|
||||
auto keybag_issuer_itr = mIssuerSigner.find(mCertChain[i].signature.issuer);
|
||||
auto local_issuer_itr = mCertImportedIssuerSigner.find(mCertChain[i].signature.issuer);
|
||||
|
||||
// try first with the keybag imported issuer
|
||||
if (keybag_issuer_itr != mIssuerSigner.end() && keybag_issuer_itr->second->getSigType() == mCertChain[i].signature.sig_type)
|
||||
{
|
||||
mCertSigValid[i] = keybag_issuer_itr->second->verifyHash(mCertChain[i].calculated_hash.data(), mCertChain[i].signature.sig.data()) ? ValidState::Good : ValidState::Fail;
|
||||
}
|
||||
// fallback try with the issuer profiles imported from the local certificates
|
||||
else if (local_issuer_itr != mCertImportedIssuerSigner.end() && local_issuer_itr->second->getSigType() == mCertChain[i].signature.sig_type)
|
||||
{
|
||||
mCertSigValid[i] = local_issuer_itr->second->verifyHash(mCertChain[i].calculated_hash.data(), mCertChain[i].signature.sig.data()) ? ValidState::Good : ValidState::Fail;
|
||||
}
|
||||
else
|
||||
{
|
||||
// cannot locate rsa key to verify
|
||||
fmt::print(stderr, "Could not read public key for \"{}\" (certificate).\n", mCertChain[i].signature.issuer);
|
||||
mCertSigValid[i] = ValidState::Fail;
|
||||
}
|
||||
}
|
||||
|
||||
// verify tmd
|
||||
{
|
||||
auto keybag_issuer_itr = mIssuerSigner.find(mTitleMetaData.signature.issuer);
|
||||
auto local_issuer_itr = mCertImportedIssuerSigner.find(mTitleMetaData.signature.issuer);
|
||||
|
||||
// try first with the keybag imported issuer
|
||||
if (keybag_issuer_itr != mIssuerSigner.end() && keybag_issuer_itr->second->getSigType() == mTitleMetaData.signature.sig_type)
|
||||
{
|
||||
mTitleMetaDataSigValid = keybag_issuer_itr->second->verifyHash(mTitleMetaData.calculated_hash.data(), mTitleMetaData.signature.sig.data()) ? ValidState::Good : ValidState::Fail;
|
||||
}
|
||||
// fallback try with the issuer profiles imported from the local certificates
|
||||
else if (local_issuer_itr != mCertImportedIssuerSigner.end() && local_issuer_itr->second->getSigType() == mTitleMetaData.signature.sig_type)
|
||||
{
|
||||
mTitleMetaDataSigValid = local_issuer_itr->second->verifyHash(mTitleMetaData.calculated_hash.data(), mTitleMetaData.signature.sig.data()) ? ValidState::Good : ValidState::Fail;
|
||||
}
|
||||
else
|
||||
{
|
||||
// cannot locate rsa key to verify
|
||||
fmt::print(stderr, "Could not read public key for \"{}\" (tmd).\n", mTitleMetaData.signature.issuer);
|
||||
mTitleMetaDataSigValid = ValidState::Fail;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ctrtool::TmdProcess::printData()
|
||||
{
|
||||
{
|
||||
fmt::print("TitleMetaData:\n");
|
||||
fmt::print("|- DigitalSignature: {:s}\n", getValidString(mTitleMetaDataSigValid));
|
||||
fmt::print("| |- SigType: {:s} (0x{:x})\n", getSigTypeString(mTitleMetaData.signature.sig_type), (uint32_t)mTitleMetaData.signature.sig_type);
|
||||
fmt::print("| |- Issuer: {:s}\n", mTitleMetaData.signature.issuer);
|
||||
fmt::print("| \\- Signature: {:s}\n", getTruncatedBytesString(mTitleMetaData.signature.sig.data(), mTitleMetaData.signature.sig.size(), mVerbose));
|
||||
fmt::print("|- TitleId: {:016x}\n", mTitleMetaData.title_id);
|
||||
fmt::print("|- TitleVersion: {} ({:d})\n", getTitleVersionString(mTitleMetaData.title_version), mTitleMetaData.title_version);
|
||||
fmt::print("|- CustomData:\n");
|
||||
// TWL Title
|
||||
if (isTwlTitle(mTitleMetaData.title_id))
|
||||
{
|
||||
fmt::print("| |- PublicSaveDataSize: 0x{:x}\n", mTitleMetaData.twl_custom_data.public_save_data_size);
|
||||
fmt::print("| |- PrivateSaveDataSize: 0x{:x}\n", mTitleMetaData.twl_custom_data.private_save_data_size);
|
||||
fmt::print("| \\- Flag: 0x{:02x}\n", mTitleMetaData.twl_custom_data.flag);
|
||||
}
|
||||
// CTR
|
||||
else
|
||||
{
|
||||
fmt::print("| |- SaveDataSize: 0x{:x}\n", mTitleMetaData.ctr_custom_data.save_data_size);
|
||||
fmt::print("| \\- IsSnakeOnly: {}\n", mTitleMetaData.ctr_custom_data.is_snake_only);
|
||||
}
|
||||
fmt::print("\\- ContentInfo:\n");
|
||||
for (size_t i = 0; i < mTitleMetaData.content_info.size(); i++)
|
||||
{
|
||||
fmt::print(" {:1}- 0x{:04x}:\n", (i+1 < mTitleMetaData.content_info.size() ? "|" : "\\"), mTitleMetaData.content_info[i].index);
|
||||
fmt::print(" {:1} |- ContentId: 0x{:08x}\n", (i+1 < mTitleMetaData.content_info.size() ? "|" : ""), mTitleMetaData.content_info[i].id);
|
||||
fmt::print(" {:1} |- Encrypted: {}\n", (i+1 < mTitleMetaData.content_info.size() ? "|" : ""), (mTitleMetaData.content_info[i].is_encrypted ? "YES" : "NO"));
|
||||
fmt::print(" {:1} |- Optional: {}\n", (i+1 < mTitleMetaData.content_info.size() ? "|" : ""), (mTitleMetaData.content_info[i].is_optional ? "YES" : "NO"));
|
||||
fmt::print(" {:1} |- Size: 0x{:x}\n", (i+1 < mTitleMetaData.content_info.size() ? "|" : ""), mTitleMetaData.content_info[i].size);
|
||||
fmt::print(" {:1} \\- Hash: {}\n", (i+1 < mTitleMetaData.content_info.size() ? "|" : ""),
|
||||
tc::cli::FormatUtil::formatBytesAsString(mTitleMetaData.content_info[i].hash.data(), mTitleMetaData.content_info[i].hash.size(), true, ""));
|
||||
}
|
||||
}
|
||||
if (mCertChain.size() > 0)
|
||||
{
|
||||
fmt::print("Certificate Chain:\n");
|
||||
for (size_t i = 0; i < mCertChain.size(); i++)
|
||||
{
|
||||
#define _CERT_FORMAT_MACRO(x,y) (i+1 < mCertChain.size() ? (x) : (y))
|
||||
|
||||
fmt::print("{:1}- Certificate {:d}:\n", _CERT_FORMAT_MACRO("|","\\"), i);
|
||||
fmt::print("{:1} |- DigitalSignature: {:s}\n", _CERT_FORMAT_MACRO("|"," "), getValidString(mCertSigValid[i]));
|
||||
fmt::print("{:1} | |- SigType: {:s} (0x{:x})\n", _CERT_FORMAT_MACRO("|"," "), getSigTypeString(mCertChain[i].signature.sig_type), (uint32_t)mCertChain[i].signature.sig_type);
|
||||
fmt::print("{:1} | |- Issuer: {:s}\n", _CERT_FORMAT_MACRO("|"," "), mCertChain[i].signature.issuer);
|
||||
fmt::print("{:1} | \\- Signature: {:s}\n", _CERT_FORMAT_MACRO("|"," "), getTruncatedBytesString(mCertChain[i].signature.sig.data(), mCertChain[i].signature.sig.size(), mVerbose));
|
||||
fmt::print("{:1} |- Subject: {:s}\n", _CERT_FORMAT_MACRO("|"," "), mCertChain[i].subject);
|
||||
//fmt::print("{:1} |- Date: {:d}\n", _CERT_FORMAT_MACRO("|"," "), mCertChain[i].date);
|
||||
fmt::print("{:1} \\- PublicKey: {:s} (0x{:x})\n", _CERT_FORMAT_MACRO("|"," "), getCertificatePublicKeyTypeString(mCertChain[i].public_key_type), (uint32_t)mCertChain[i].public_key_type);
|
||||
switch (mCertChain[i].public_key_type)
|
||||
{
|
||||
case brd::es::ESCertPubKeyType::RSA4096:
|
||||
fmt::print("{:1} |- m: {:s}\n", _CERT_FORMAT_MACRO("|"," "), getTruncatedBytesString(mCertChain[i].rsa4096_public_key.m.data(), mCertChain[i].rsa4096_public_key.m.size(), mVerbose));
|
||||
fmt::print("{:1} \\- e: {:s}\n", _CERT_FORMAT_MACRO("|"," "), getTruncatedBytesString(mCertChain[i].rsa4096_public_key.e.data(), mCertChain[i].rsa4096_public_key.e.size(), mVerbose));
|
||||
break;
|
||||
case brd::es::ESCertPubKeyType::RSA2048:
|
||||
fmt::print("{:1} |- m: {:s}\n", _CERT_FORMAT_MACRO("|"," "), getTruncatedBytesString(mCertChain[i].rsa2048_public_key.m.data(), mCertChain[i].rsa2048_public_key.m.size(), mVerbose));
|
||||
fmt::print("{:1} \\- e: {:s}\n", _CERT_FORMAT_MACRO("|"," "), getTruncatedBytesString(mCertChain[i].rsa2048_public_key.e.data(), mCertChain[i].rsa2048_public_key.e.size(), mVerbose));
|
||||
break;
|
||||
case brd::es::ESCertPubKeyType::ECC:
|
||||
fmt::print("{:1} |- x: {:s}\n", _CERT_FORMAT_MACRO("|"," "), getTruncatedBytesString(mCertChain[i].ecc233_public_key.x.data(), mCertChain[i].ecc233_public_key.x.size(), mVerbose));
|
||||
fmt::print("{:1} \\- y: {:s}\n", _CERT_FORMAT_MACRO("|"," "), getTruncatedBytesString(mCertChain[i].ecc233_public_key.y.data(), mCertChain[i].ecc233_public_key.y.size(), mVerbose));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
#undef _CERT_FORMAT_MACRO
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool ctrtool::TmdProcess::isTwlTitle(uint64_t title_id)
|
||||
{
|
||||
return ((title_id >> 47) & 1) == 1;
|
||||
}
|
||||
|
||||
std::string ctrtool::TmdProcess::getValidString(byte_t validstate)
|
||||
{
|
||||
std::string ret_str;
|
||||
|
||||
switch (validstate)
|
||||
{
|
||||
case ValidState::Unchecked:
|
||||
ret_str = "";
|
||||
break;
|
||||
case ValidState::Good:
|
||||
ret_str = "(GOOD)";
|
||||
break;
|
||||
case ValidState::Fail:
|
||||
default:
|
||||
ret_str = "(FAIL)";
|
||||
break;
|
||||
}
|
||||
|
||||
return ret_str;
|
||||
}
|
||||
|
||||
std::string ctrtool::TmdProcess::getTruncatedBytesString(const byte_t* data, size_t len, bool do_not_truncate)
|
||||
{
|
||||
if (data == nullptr) { return fmt::format(""); }
|
||||
|
||||
std::string str = "";
|
||||
|
||||
if (len <= 8 || do_not_truncate)
|
||||
{
|
||||
str = tc::cli::FormatUtil::formatBytesAsString(data, len, true, "");
|
||||
}
|
||||
else
|
||||
{
|
||||
str = fmt::format("{:02X}{:02X}{:02X}{:02X}...{:02X}{:02X}{:02X}{:02X}", data[0], data[1], data[2], data[3], data[len-4], data[len-3], data[len-2], data[len-1]);
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
std::string ctrtool::TmdProcess::getSigTypeString(brd::es::ESSigType sig_type)
|
||||
{
|
||||
std::string ret_str;
|
||||
|
||||
switch (sig_type)
|
||||
{
|
||||
case brd::es::ESSigType::RSA4096_SHA1:
|
||||
ret_str = "RSA-4096-SHA1";
|
||||
break;
|
||||
case brd::es::ESSigType::RSA2048_SHA1:
|
||||
ret_str = "RSA-2048-SHA1";
|
||||
break;
|
||||
case brd::es::ESSigType::ECC_SHA1:
|
||||
ret_str = "ECDSA-233-SHA1";
|
||||
break;
|
||||
case brd::es::ESSigType::RSA4096_SHA256:
|
||||
ret_str = "RSA-4096-SHA256";
|
||||
break;
|
||||
case brd::es::ESSigType::RSA2048_SHA256:
|
||||
ret_str = "RSA-2048-SHA256";
|
||||
break;
|
||||
case brd::es::ESSigType::ECC_SHA256:
|
||||
ret_str = "ECDSA-233-SHA256";
|
||||
break;
|
||||
default:
|
||||
ret_str = fmt::format("0x{:x}", (uint32_t)sig_type);
|
||||
}
|
||||
|
||||
return ret_str;
|
||||
}
|
||||
|
||||
std::string ctrtool::TmdProcess::getCertificatePublicKeyTypeString(brd::es::ESCertPubKeyType public_key_type)
|
||||
{
|
||||
std::string ret_str;
|
||||
|
||||
switch (public_key_type)
|
||||
{
|
||||
case brd::es::ESCertPubKeyType::RSA4096:
|
||||
ret_str = "RSA-4096";
|
||||
break;
|
||||
case brd::es::ESCertPubKeyType::RSA2048:
|
||||
ret_str = "RSA-2048";
|
||||
break;
|
||||
case brd::es::ESCertPubKeyType::ECC:
|
||||
ret_str = "ECC-233";
|
||||
break;
|
||||
default:
|
||||
ret_str = fmt::format("0x{:x}", (uint32_t)public_key_type);
|
||||
}
|
||||
|
||||
return ret_str;
|
||||
}
|
||||
|
||||
std::string ctrtool::TmdProcess::getTitleVersionString(uint16_t version)
|
||||
{
|
||||
return fmt::format("{major:d}.{minor:d}.{build:d}", fmt::arg("major", (uint32_t)((version >> 10) & 0x3F)), fmt::arg("minor", (uint32_t)((version >> 4) & 0x3F)), fmt::arg("build", (uint32_t)(version & 0xF)));
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
#include "types.h"
|
||||
#include "KeyBag.h"
|
||||
#include <tc/Optional.h>
|
||||
|
||||
#include <ntd/n3ds/es/RsaSigner.h>
|
||||
#include <ntd/n3ds/es/Certificate.h>
|
||||
#include <ntd/n3ds/es/TitleMetaData.h>
|
||||
|
||||
namespace ctrtool {
|
||||
|
||||
class TmdProcess
|
||||
{
|
||||
public:
|
||||
TmdProcess();
|
||||
|
||||
void setInputStream(const std::shared_ptr<tc::io::IStream>& input_stream);
|
||||
void setKeyBag(const ctrtool::KeyBag& key_bag);
|
||||
void setCliOutputMode(bool show_header_info);
|
||||
void setVerboseMode(bool verbose);
|
||||
void setVerifyMode(bool verify);
|
||||
|
||||
void process();
|
||||
private:
|
||||
std::string mModuleLabel;
|
||||
|
||||
// input args
|
||||
std::shared_ptr<tc::io::IStream> mInputStream;
|
||||
ctrtool::KeyBag mKeyBag;
|
||||
bool mShowInfo;
|
||||
bool mVerbose;
|
||||
bool mVerify;
|
||||
|
||||
// process variables
|
||||
std::map<std::string, std::shared_ptr<ntd::n3ds::es::ISigner>> mIssuerSigner;
|
||||
std::map<std::string, std::shared_ptr<ntd::n3ds::es::ISigner>> mCertImportedIssuerSigner;
|
||||
|
||||
std::vector<ntd::n3ds::es::Certificate> mCertChain;
|
||||
std::vector<ValidState> mCertSigValid;
|
||||
|
||||
ntd::n3ds::es::TitleMetaData mTitleMetaData;
|
||||
ValidState mTitleMetaDataSigValid;
|
||||
|
||||
// helper methods
|
||||
void importIssuerProfiles();
|
||||
void importData();
|
||||
void verifyData();
|
||||
void printData();
|
||||
|
||||
bool isTwlTitle(uint64_t title_id);
|
||||
|
||||
// string utils
|
||||
std::string getValidString(byte_t validstate);
|
||||
std::string getTruncatedBytesString(const byte_t* data, size_t len, bool do_not_truncate = false);
|
||||
std::string getSigTypeString(brd::es::ESSigType sig_type);
|
||||
std::string getCertificatePublicKeyTypeString(brd::es::ESCertPubKeyType public_key_type);
|
||||
std::string getTitleVersionString(uint16_t version);
|
||||
};
|
||||
|
||||
}
|
||||
@@ -12,6 +12,8 @@
|
||||
#include "LzssProcess.h"
|
||||
#include "CrrProcess.h"
|
||||
#include "FirmProcess.h"
|
||||
#include "TikProcess.h"
|
||||
#include "TmdProcess.h"
|
||||
|
||||
#include <tc/io/SubStream.h>
|
||||
#include <ntd/n3ds/IvfcStream.h>
|
||||
@@ -182,6 +184,26 @@ int umain(const std::vector<std::string>& args, const std::vector<std::string>&
|
||||
proc.setFirmwareType(set.firm.firm_type);
|
||||
proc.process();
|
||||
}
|
||||
else if (set.infile.filetype == ctrtool::Settings::FILE_TYPE_TIK)
|
||||
{
|
||||
ctrtool::TikProcess proc;
|
||||
proc.setInputStream(infile_stream);
|
||||
proc.setKeyBag(set.opt.keybag);
|
||||
proc.setCliOutputMode(true);
|
||||
proc.setVerboseMode(set.opt.verbose);
|
||||
proc.setVerifyMode(set.opt.verify);
|
||||
proc.process();
|
||||
}
|
||||
else if (set.infile.filetype == ctrtool::Settings::FILE_TYPE_TMD)
|
||||
{
|
||||
ctrtool::TmdProcess proc;
|
||||
proc.setInputStream(infile_stream);
|
||||
proc.setKeyBag(set.opt.keybag);
|
||||
proc.setCliOutputMode(true);
|
||||
proc.setVerboseMode(set.opt.verbose);
|
||||
proc.setVerifyMode(set.opt.verify);
|
||||
proc.process();
|
||||
}
|
||||
|
||||
switch (set.infile.filetype)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user