From c029db1363b06199b9383243d580d91dd534fa08 Mon Sep 17 00:00:00 2001 From: Michael Scire Date: Tue, 6 Feb 2018 00:04:27 -0800 Subject: [PATCH] Implement support for external keys (closes #6) --- .gitignore | 3 - Makefile | 4 +- README.md | 11 +++ extkeys.c | 258 +++++++++++++++++++++++++++++++++++++++++++++++++++++ extkeys.h | 13 +++ main.c | 80 ++++++++--------- settings.h | 1 + 7 files changed, 324 insertions(+), 46 deletions(-) create mode 100644 extkeys.c create mode 100644 extkeys.h diff --git a/.gitignore b/.gitignore index 14d6d33..fd6060e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,6 @@ /config.mk /hactool /hactool.exe -/pki_zero.c -/pki_full.c -/pki_staging.c /*.o /*.dll /bin diff --git a/Makefile b/Makefile index 194da86..47df914 100644 --- a/Makefile +++ b/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 diff --git a/README.md b/README.md index 67478d5..e4e74d3 100644 --- a/README.md +++ b/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. diff --git a/extkeys.c b/extkeys.c new file mode 100644 index 0000000..751c1d2 --- /dev/null +++ b/extkeys.c @@ -0,0 +1,258 @@ +#include +#include +#include +#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); + } + } + } +} + diff --git a/extkeys.h b/extkeys.h new file mode 100644 index 0000000..09ee7c8 --- /dev/null +++ b/extkeys.h @@ -0,0 +1,13 @@ +#ifndef HACTOOL_EXTKEYS_H +#define HACTOOL_EXTKEYS_H + +#include +#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 \ No newline at end of file diff --git a/main.c b/main.c index 8e35888..b0e7201 100644 --- a/main.c +++ b/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. */ diff --git a/settings.h b/settings.h index 63e54e6..70d7480 100644 --- a/settings.h +++ b/settings.h @@ -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. */