mirror of
https://github.com/SciresM/hactool
synced 2024-11-21 19:53:01 +00:00
Implement support for external keys (closes #6)
This commit is contained in:
parent
54263ae821
commit
c029db1363
7 changed files with 324 additions and 46 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,9 +1,6 @@
|
|||
/config.mk
|
||||
/hactool
|
||||
/hactool.exe
|
||||
/pki_zero.c
|
||||
/pki_full.c
|
||||
/pki_staging.c
|
||||
/*.o
|
||||
/*.dll
|
||||
/bin
|
||||
|
|
4
Makefile
4
Makefile
|
@ -13,13 +13,15 @@ all:
|
|||
.c.o:
|
||||
$(CC) $(INCLUDE) -c $(CFLAGS) -o $@ $<
|
||||
|
||||
hactool: sha.o aes.o rsa.o npdm.o bktr.o pki.o pfs0.o hfs0.o romfs.o utils.o nca.o xci.o main.o filepath.o ConvertUTF.o
|
||||
hactool: sha.o aes.o extkeys.o rsa.o npdm.o bktr.o pki.o pfs0.o hfs0.o romfs.o utils.o nca.o xci.o main.o filepath.o ConvertUTF.o
|
||||
$(CC) -o $@ $^ $(LDFLAGS) -L $(LIBDIR)
|
||||
|
||||
aes.o: aes.h types.h
|
||||
|
||||
bktr.o: bktr.h types.h
|
||||
|
||||
extkeys.o: extkeys.h types.h settings.h
|
||||
|
||||
filepath.o: filepath.c types.h
|
||||
|
||||
hfs0.o: hfs0.h types.h
|
||||
|
|
11
README.md
11
README.md
|
@ -18,6 +18,7 @@ Options:
|
|||
-r, --raw Keep raw data, don't unpack.
|
||||
-y, --verify Verify hashes and signatures.
|
||||
-d, --dev Decrypt with development keys instead of retail.
|
||||
-k, --keyset Load keys from an external file.
|
||||
-t, --intype=type Specify input file type [nca, xci, pfs0, romfs, hfs0]
|
||||
--titlekey=key Set title key for Rights ID crypto titles.
|
||||
--contentkey=key Set raw key for NCA body decryption.
|
||||
|
@ -66,6 +67,16 @@ If your `make` is not GNU make (e.g. on BSD variants), you need to call `gmake`
|
|||
|
||||
If on Windows, I recommend using MinGW.
|
||||
|
||||
## External Keys
|
||||
|
||||
External keys can be provided by the -k/--keyset argument to the a keyset filename.
|
||||
Keyset files are text files containing one key per line, in the form "key_name = HEXADECIMALKEY".
|
||||
Case shouldn't matter, nor should whitespace.
|
||||
|
||||
In addition, if -k/--keyset is not set, hactool will check for the presence of a keyset file
|
||||
in $HOME/.switch/prod.keys (or $HOME/.switch/dev.keys if -d/--dev is set). If present, this file
|
||||
will automatically be loaded.
|
||||
|
||||
## Licensing
|
||||
|
||||
This software is licensed under the terms of the ISC License.
|
||||
|
|
258
extkeys.c
Normal file
258
extkeys.c
Normal file
|
@ -0,0 +1,258 @@
|
|||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "extkeys.h"
|
||||
|
||||
/**
|
||||
* Reads a line from file f and parses out the key and value from it.
|
||||
* The format of a line must match /^ *[A-Za-z0-9_] *[,=] *.+$/.
|
||||
* If a line ends in \r, the final \r is stripped.
|
||||
* The input file is assumed to have been opened with the 'b' flag.
|
||||
* The input file is assumed to contain only ASCII.
|
||||
*
|
||||
* A line cannot exceed 512 bytes in length.
|
||||
* Lines that are excessively long will be silently truncated.
|
||||
*
|
||||
* On success, *key and *value will be set to point to the key and value in
|
||||
* the input line, respectively.
|
||||
* *key and *value may also be NULL in case of empty lines.
|
||||
* On failure, *key and *value will be set to NULL.
|
||||
* End of file is considered failure.
|
||||
*
|
||||
* Because *key and *value will point to a static buffer, their contents must be
|
||||
* copied before calling this function again.
|
||||
* For the same reason, this function is not thread-safe.
|
||||
*
|
||||
* The key will be converted to lowercase.
|
||||
* An empty key is considered a parse error, but an empty value is returned as
|
||||
* success.
|
||||
*
|
||||
* This function assumes that the file can be trusted not to contain any NUL in
|
||||
* the contents.
|
||||
*
|
||||
* Whitespace (' ', ASCII 0x20, as well as '\t', ASCII 0x09) at the beginning of
|
||||
* the line, at the end of the line as well as around = (or ,) will be ignored.
|
||||
*
|
||||
* @param f the file to read
|
||||
* @param key pointer to change to point to the key
|
||||
* @param value pointer to change to point to the value
|
||||
* @return 0 on success,
|
||||
* 1 on end of file,
|
||||
* -1 on parse error (line too long, line malformed)
|
||||
* -2 on I/O error
|
||||
*/
|
||||
static int get_kv(FILE *f, char **key, char **value) {
|
||||
#define SKIP_SPACE(p) do {\
|
||||
for (; *p == ' ' || *p == '\t'; ++p)\
|
||||
;\
|
||||
} while(0);
|
||||
static char line[512];
|
||||
char *k, *v, *p, *end;
|
||||
|
||||
*key = *value = NULL;
|
||||
|
||||
errno = 0;
|
||||
if (fgets(line, (int)sizeof(line), f) == NULL) {
|
||||
if (feof(f))
|
||||
return 1;
|
||||
else
|
||||
return -2;
|
||||
}
|
||||
if (errno != 0)
|
||||
return -2;
|
||||
|
||||
if (*line == '\n' || *line == '\r' || *line == '\0')
|
||||
return 0;
|
||||
|
||||
/* Not finding \r or \n is not a problem.
|
||||
* The line might just be exactly 512 characters long, we have no way to
|
||||
* tell.
|
||||
* Additionally, it's possible that the last line of a file is not actually
|
||||
* a line (i.e., does not end in '\n'); we do want to handle those.
|
||||
*/
|
||||
if ((p = strchr(line, '\r')) != NULL || (p = strchr(line, '\n')) != NULL) {
|
||||
end = p;
|
||||
*p = '\0';
|
||||
} else {
|
||||
end = line + strlen(line) + 1;
|
||||
}
|
||||
|
||||
p = line;
|
||||
SKIP_SPACE(p);
|
||||
k = p;
|
||||
|
||||
/* Validate key and convert to lower case. */
|
||||
for (; *p != ' ' && *p != ',' && *p != '\t' && *p != '='; ++p) {
|
||||
if (*p == '\0')
|
||||
return -1;
|
||||
|
||||
if (*p >= 'A' && *p <= 'Z') {
|
||||
*p = 'a' + (*p - 'A');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (*p != '_' &&
|
||||
(*p < '0' && *p > '9') &&
|
||||
(*p < 'a' && *p > 'z'))
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Bail if the final ++p put us at the end of string */
|
||||
if (*p == '\0')
|
||||
return -1;
|
||||
|
||||
/* We should be at the end of key now and either whitespace or [,=]
|
||||
* follows.
|
||||
*/
|
||||
if (*p == '=' || *p == ',') {
|
||||
*p++ = '\0';
|
||||
} else {
|
||||
*p++ = '\0';
|
||||
SKIP_SPACE(p);
|
||||
if (*p != '=' && *p != ',')
|
||||
return -1;
|
||||
*p++ = '\0';
|
||||
}
|
||||
|
||||
/* Empty key is an error. */
|
||||
if (*k == '\0')
|
||||
return -1;
|
||||
|
||||
SKIP_SPACE(p);
|
||||
v = p;
|
||||
|
||||
/* Skip trailing whitespace */
|
||||
for (p = end - 1; *p == '\t' || *p == ' '; --p)
|
||||
;
|
||||
|
||||
*(p + 1) = '\0';
|
||||
|
||||
*key = k;
|
||||
*value = v;
|
||||
|
||||
return 0;
|
||||
#undef SKIP_SPACE
|
||||
}
|
||||
|
||||
static int ishex(char c) {
|
||||
if ('a' <= c && c <= 'f') return 1;
|
||||
if ('A' <= c && c <= 'F') return 1;
|
||||
if ('0' <= c && c <= '9') return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static char hextoi(char c) {
|
||||
if ('a' <= c && c <= 'f') return c - 'a' + 0xA;
|
||||
if ('A' <= c && c <= 'F') return c - 'A' + 0xA;
|
||||
if ('0' <= c && c <= '9') return c - '0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
void parse_hex_key(unsigned char *key, const char *hex, unsigned int len) {
|
||||
if (strlen(hex) != 2 * len) {
|
||||
fprintf(stderr, "Key (%s) must be %"PRIu32" hex digits!\n", hex, 2 * len);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
for (unsigned int i = 0; i < 2 * len; i++) {
|
||||
if (!ishex(hex[i])) {
|
||||
fprintf(stderr, "Key (%s) must be %"PRIu32" hex digits!\n", hex, 2 * len);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
memset(key, 0, len);
|
||||
|
||||
for (unsigned int i = 0; i < 2 * len; i++) {
|
||||
char val = hextoi(hex[i]);
|
||||
if ((i & 1) == 0) {
|
||||
val <<= 4;
|
||||
}
|
||||
key[i >> 1] |= val;
|
||||
}
|
||||
}
|
||||
|
||||
void extkeys_initialize_keyset(nca_keyset_t *keyset, FILE *f) {
|
||||
char *key, *value;
|
||||
int ret;
|
||||
|
||||
while ((ret = get_kv(f, &key, &value)) != 1 && ret != -2) {
|
||||
if (ret == 0) {
|
||||
if (key == NULL || value == NULL) {
|
||||
continue;
|
||||
}
|
||||
int matched_key = 0;
|
||||
if (strcmp(key, "aes_kek_generation_source") == 0) {
|
||||
parse_hex_key(keyset->aes_kek_generation_source, value, sizeof(keyset->aes_kek_generation_source));
|
||||
matched_key = 1;
|
||||
} else if (strcmp(key, "aes_key_generation_source") == 0) {
|
||||
parse_hex_key(keyset->aes_kek_generation_source, value, sizeof(keyset->aes_kek_generation_source));
|
||||
matched_key = 1;
|
||||
} else if (strcmp(key, "key_area_key_application_source") == 0) {
|
||||
parse_hex_key(keyset->key_area_key_application_source, value, sizeof(keyset->key_area_key_application_source));
|
||||
matched_key = 1;
|
||||
} else if (strcmp(key, "key_area_key_ocean_source") == 0) {
|
||||
parse_hex_key(keyset->key_area_key_ocean_source, value, sizeof(keyset->key_area_key_ocean_source));
|
||||
matched_key = 1;
|
||||
} else if (strcmp(key, "key_area_key_system_source") == 0) {
|
||||
parse_hex_key(keyset->key_area_key_system_source, value, sizeof(keyset->key_area_key_system_source));
|
||||
matched_key = 1;
|
||||
} else if (strcmp(key, "titlekek_source") == 0) {
|
||||
parse_hex_key(keyset->titlekek_source, value, sizeof(keyset->titlekek_source));
|
||||
matched_key = 1;
|
||||
} else if (strcmp(key, "header_kek_source") == 0) {
|
||||
parse_hex_key(keyset->header_kek_source, value, sizeof(keyset->header_kek_source));
|
||||
matched_key = 1;
|
||||
} else if (strcmp(key, "header_key_source") == 0) {
|
||||
parse_hex_key(keyset->encrypted_header_key, value, sizeof(keyset->encrypted_header_key));
|
||||
matched_key = 1;
|
||||
} else if (strcmp(key, "header_key") == 0) {
|
||||
parse_hex_key(keyset->header_key, value, sizeof(keyset->header_key));
|
||||
matched_key = 1;
|
||||
} else {
|
||||
char test_name[0x100];
|
||||
memset(test_name, 0, sizeof(100));
|
||||
for (unsigned int i = 0; i < 0x20 && !matched_key; i++) {
|
||||
snprintf(test_name, sizeof(test_name), "master_key_%02"PRIx32, i);
|
||||
if (strcmp(key, test_name) == 0) {
|
||||
parse_hex_key(keyset->master_keys[i], value, sizeof(keyset->master_keys[i]));
|
||||
matched_key = 1;
|
||||
break;
|
||||
}
|
||||
|
||||
snprintf(test_name, sizeof(test_name), "titlekek_%02"PRIx32, i);
|
||||
if (strcmp(key, test_name) == 0) {
|
||||
parse_hex_key(keyset->titlekeks[i], value, sizeof(keyset->titlekeks[i]));
|
||||
matched_key = 1;
|
||||
break;
|
||||
}
|
||||
|
||||
snprintf(test_name, sizeof(test_name), "key_area_key_application_%02"PRIx32, i);
|
||||
if (strcmp(key, test_name) == 0) {
|
||||
parse_hex_key(keyset->key_area_keys[i][0], value, sizeof(keyset->key_area_keys[i][0]));
|
||||
matched_key = 1;
|
||||
break;
|
||||
}
|
||||
|
||||
snprintf(test_name, sizeof(test_name), "key_area_key_ocean_%02"PRIx32, i);
|
||||
if (strcmp(key, test_name) == 0) {
|
||||
parse_hex_key(keyset->key_area_keys[i][1], value, sizeof(keyset->key_area_keys[i][1]));
|
||||
matched_key = 1;
|
||||
break;
|
||||
}
|
||||
|
||||
snprintf(test_name, sizeof(test_name), "key_area_key_system_%02"PRIx32, i);
|
||||
if (strcmp(key, test_name) == 0) {
|
||||
parse_hex_key(keyset->key_area_keys[i][2], value, sizeof(keyset->key_area_keys[i][2]));
|
||||
matched_key = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!matched_key) {
|
||||
fprintf(stderr, "[WARN]: Failed to match key \"%s\", (value \"%s\")\n", key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
13
extkeys.h
Normal file
13
extkeys.h
Normal file
|
@ -0,0 +1,13 @@
|
|||
#ifndef HACTOOL_EXTKEYS_H
|
||||
#define HACTOOL_EXTKEYS_H
|
||||
|
||||
#include <string.h>
|
||||
#include "types.h"
|
||||
#include "utils.h"
|
||||
#include "settings.h"
|
||||
|
||||
void parse_hex_key(unsigned char *key, const char *hex, unsigned int len);
|
||||
void extkeys_initialize_keyset(nca_keyset_t *keyset, FILE *f);
|
||||
|
||||
|
||||
#endif
|
80
main.c
80
main.c
|
@ -9,6 +9,7 @@
|
|||
#include "pki.h"
|
||||
#include "nca.h"
|
||||
#include "xci.h"
|
||||
#include "extkeys.h"
|
||||
|
||||
static char *prog_name = "hactool";
|
||||
|
||||
|
@ -27,6 +28,7 @@ static void usage(void) {
|
|||
" -r, --raw Keep raw data, don't unpack.\n"
|
||||
" -y, --verify Verify hashes and signatures.\n"
|
||||
" -d, --dev Decrypt with development keys instead of retail.\n"
|
||||
" -k, --keyset Load keys from an external file.\n"
|
||||
" -t, --intype=type Specify input file type [nca, xci, pfs0, romfs, hfs0]\n"
|
||||
" --titlekey=key Set title key for Rights ID crypto titles.\n"
|
||||
" --contentkey=key Set raw key for NCA body decryption.\n"
|
||||
|
@ -70,56 +72,20 @@ static void usage(void) {
|
|||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
static int ishex(char c) {
|
||||
if ('a' <= c && c <= 'f') return 1;
|
||||
if ('A' <= c && c <= 'F') return 1;
|
||||
if ('0' <= c && c <= '9') return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static char hextoi(char c) {
|
||||
if ('a' <= c && c <= 'f') return c - 'a' + 0xA;
|
||||
if ('A' <= c && c <= 'F') return c - 'A' + 0xA;
|
||||
if ('0' <= c && c <= '9') return c - '0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
void parse_hex_key(unsigned char *key, const char *hex) {
|
||||
if (strlen(hex) != 32) {
|
||||
fprintf(stderr, "Key must be 32 hex digits!\n");
|
||||
usage();
|
||||
}
|
||||
|
||||
for (unsigned int i = 0; i < 32; i++) {
|
||||
if (!ishex(hex[i])) {
|
||||
fprintf(stderr, "Key must be 32 hex digits!\n");
|
||||
usage();
|
||||
}
|
||||
}
|
||||
|
||||
memset(key, 0, 16);
|
||||
|
||||
for (unsigned int i = 0; i < 32; i++) {
|
||||
char val = hextoi(hex[i]);
|
||||
if ((i & 1) == 0) {
|
||||
val <<= 4;
|
||||
}
|
||||
key[i >> 1] |= val;
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
hactool_ctx_t tool_ctx;
|
||||
hactool_ctx_t base_ctx; /* Context for base NCA, if used. */
|
||||
nca_ctx_t nca_ctx;
|
||||
char input_name[0x200];
|
||||
|
||||
filepath_t keypath;
|
||||
|
||||
prog_name = (argc < 1) ? "hactool" : argv[0];
|
||||
|
||||
nca_init(&nca_ctx);
|
||||
memset(&tool_ctx, 0, sizeof(tool_ctx));
|
||||
memset(&base_ctx, 0, sizeof(base_ctx));
|
||||
memset(input_name, 0, sizeof(input_name));
|
||||
filepath_init(&keypath);
|
||||
nca_ctx.tool_ctx = &tool_ctx;
|
||||
|
||||
nca_ctx.tool_ctx->file_type = FILETYPE_NCA;
|
||||
|
@ -139,6 +105,7 @@ int main(int argc, char **argv) {
|
|||
{"verify", 0, NULL, 'y'},
|
||||
{"raw", 0, NULL, 'r'},
|
||||
{"intype", 1, NULL, 't'},
|
||||
{"keyset", 1, NULL, 'k'},
|
||||
{"section0", 1, NULL, 0},
|
||||
{"section1", 1, NULL, 1},
|
||||
{"section2", 1, NULL, 2},
|
||||
|
@ -168,7 +135,7 @@ int main(int argc, char **argv) {
|
|||
{NULL, 0, NULL, 0},
|
||||
};
|
||||
|
||||
c = getopt_long(argc, argv, "dryxt:i", long_options, &option_index);
|
||||
c = getopt_long(argc, argv, "dryxt:ik:", long_options, &option_index);
|
||||
if (c == -1)
|
||||
break;
|
||||
|
||||
|
@ -188,6 +155,10 @@ int main(int argc, char **argv) {
|
|||
break;
|
||||
case 'd':
|
||||
pki_initialize_keyset(&tool_ctx.settings.keyset, KEYSET_DEV);
|
||||
nca_ctx.tool_ctx->action |= ACTION_DEV;
|
||||
break;
|
||||
case 'k':
|
||||
filepath_set(&keypath, optarg);
|
||||
break;
|
||||
case 't':
|
||||
if (!strcmp(optarg, "nca")) {
|
||||
|
@ -235,11 +206,11 @@ int main(int argc, char **argv) {
|
|||
filepath_set(&nca_ctx.tool_ctx->settings.romfs_dir_path.path, optarg);
|
||||
break;
|
||||
case 12:
|
||||
parse_hex_key(nca_ctx.tool_ctx->settings.titlekey, optarg);
|
||||
parse_hex_key(nca_ctx.tool_ctx->settings.titlekey, optarg, 16);
|
||||
nca_ctx.tool_ctx->settings.has_titlekey = 1;
|
||||
break;
|
||||
case 13:
|
||||
parse_hex_key(nca_ctx.tool_ctx->settings.contentkey, optarg);
|
||||
parse_hex_key(nca_ctx.tool_ctx->settings.contentkey, optarg, 16);
|
||||
nca_ctx.tool_ctx->settings.has_contentkey = 1;
|
||||
break;
|
||||
case 14:
|
||||
|
@ -308,6 +279,31 @@ int main(int argc, char **argv) {
|
|||
return EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
/* Try to populate default keyfile. */
|
||||
if (keypath.valid == VALIDITY_INVALID) {
|
||||
char *home = getenv("HOME");
|
||||
if (home == NULL) {
|
||||
home = getenv("USERPROFILE");
|
||||
}
|
||||
if (home != NULL) {
|
||||
filepath_set(&keypath, home);
|
||||
filepath_append(&keypath, ".switch");
|
||||
filepath_append(&keypath, "%s.keys", (tool_ctx.action & ACTION_DEV) ? "dev" : "prod");
|
||||
}
|
||||
}
|
||||
|
||||
/* Load external keys, if relevant. */
|
||||
if (keypath.valid == VALIDITY_VALID) {
|
||||
FILE *keyfile = os_fopen(keypath.os_path, OS_MODE_READ);
|
||||
if (keyfile != NULL) {
|
||||
extkeys_initialize_keyset(&tool_ctx.settings.keyset, keyfile);
|
||||
fclose(keyfile);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
if (optind == argc - 1) {
|
||||
/* Copy input file. */
|
||||
|
|
|
@ -76,6 +76,7 @@ enum hactool_file_type
|
|||
#define ACTION_VERIFY (1<<2)
|
||||
#define ACTION_RAW (1<<3)
|
||||
#define ACTION_LISTROMFS (1<<4)
|
||||
#define ACTION_DEV (1<<5)
|
||||
|
||||
struct nca_ctx; /* This will get re-defined by nca.h. */
|
||||
|
||||
|
|
Loading…
Reference in a new issue