diff --git a/doc/NEWS b/doc/NEWS index 6d90216dd17..fe3adba1e1c 100644 --- a/doc/NEWS +++ b/doc/NEWS @@ -317,6 +317,8 @@ Major changes from 1.9.0-jumbo-1 (May 2019) in this bleeding-edge version: - Added a local copy of the OpenCL headers. [Alain Espinosa; 2023] +- Add Armory wallet support (accepts btcrecover data extracts). [Solar; 2024] + Major changes from 1.8.0-jumbo-1 (December 2014) to 1.9.0-jumbo-1 (May 2019): diff --git a/src/armory_fmt_plug.c b/src/armory_fmt_plug.c new file mode 100644 index 00000000000..7da88443062 --- /dev/null +++ b/src/armory_fmt_plug.c @@ -0,0 +1,429 @@ +/* + * This file is part of John the Ripper password cracker, + * Copyright (c) 2024 by Solar Designer + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted. + * + * There's ABSOLUTELY NO WARRANTY, express or implied. + */ + +#if FMT_EXTERNS_H +extern struct fmt_main fmt_armory; +#elif FMT_REGISTERS_H +john_register_one(&fmt_armory); +#else + +#include +#include +#include + +#ifdef _OPENMP +#include +#endif + +#include "arch.h" +#if !ARCH_LITTLE_ENDIAN +#include "johnswap.h" +#endif + +#include "base64_convert.h" +#include "formats.h" +#include "memory.h" +#include "crc32.h" +#include "sha2.h" +#include "aes.h" +#include "secp256k1.h" +#include "sph_ripemd.h" + +#define FORMAT_LABEL "armory" +#define FORMAT_TAG "YXI6" +#define FORMAT_TAG_LEN (sizeof(FORMAT_TAG) - 1) +#define FORMAT_NAME "Armory wallet" +#define ALGORITHM_NAME "SHA-512/AES-256/secp256k1/SHA-256/RIPEMD-160" + +#define BENCHMARK_COMMENT "" +#define BENCHMARK_LENGTH 0x107 + +#define PLAINTEXT_LENGTH 125 +#define CIPHERTEXT_LENGTH 156 + +struct custom_salt { + uint8_t privkey[32], iv[16]; + uint32_t bytes_reqd, iter_count; + uint8_t seed[32], crc[4]; +}; + +#define BINARY_SIZE 20 +#define BINARY_ALIGN 1 +#define SALT_SIZE sizeof(struct custom_salt) +#define SALT_ALIGN sizeof(uint32_t) + +#define MIN_KEYS_PER_CRYPT 1 +#define MAX_KEYS_PER_CRYPT 1 +#define OMP_SCALE 1 + +/* + * These test vectors were extracted from test wallets by Christopher Gurnee's + * btcrecover/extract-scripts/extract-armory-privkey.py + */ +static struct fmt_tests tests[] = { + /* 8 MiB, 2 iterations */ + {"YXI6DKXVcrlt7I85ct/qzlUSXX1MCs+wbXVpJdtrw9RuXZpMowuOXmfqMiuFKj0/g/G2sQScLr6+dsiNrh/OOlsDzP4T60QAAIAAAgAAAIZidMeJWIXezaAL+WpoA5nbs9eGf5syXhOnHYF65Ftmo4p+jw==", "jtr-test-password1"}, + {"YXI6zkTV2lPsGMYFxUEZNZXClHnTrbjLMgETZIcdCiIJ9HywU8wOEPJeF+jo2VOxTXb+gkV4GXrBEgaq+jX8XWFWkszRmtUAAIAAAgAAAHa4aIO3jzsfckq1ETPaJx/qVasAPzV3R9gyjqHuFqpWwB+01A==", "jtr-test-password2"}, + /* 2 MiB, 4 iterations */ + {"YXI6r7mks1qvph4G+rRT7WlIptdr9qDqyFTfXNJ3ciuWJ12BgWX5Il+y28hLNr/u4Wl49hUi4JBeq6Jz9dVBX3vAJ6476FEAACAABAAAAGGwnwXRpPbBzC5lCOBVVWDu7mUJetBOBvzVAv0IbrboDXqA8A==", "btcr-test-password"}, + /* 4 MiB, 6 iterations, generated by Armory 0.95.1 */ + {"YXI6uOgnVa0B+Wx63/haI5oBRHkDf7ge3Kjc5LXnYXtnlIUHDD+J3u5ilyHfMmo7uaFaj1cbogHyjdZUhZj3a6XzdFE3NA0AAEAABgAAAARJgevrWUGCq2+5j8v3ei+vkZOpcUhdPDwrKTyXAU4znXCL2A==", "jtr-test-password"}, + /* 32 MiB, 3 iterations */ + {"YXI6q0bSuS3K6msHGDI5wSvD04RH7VqENEsIctti29xVGbyxLoqwIyGL53+qFOkW1Zqb6ZncatQZgX/3cf9XXxjFbSxGp4EAAAACAwAAAKKiUt8yakXdpwZ6SnbPFbwIsUzUUVMgwlySR02FFtMijFRpew==", "a3q92wz8"}, + {NULL} +}; + +static struct custom_salt *saved_salt; + +static char (*saved_key)[PLAINTEXT_LENGTH + 1]; +static uint8_t (*crypt_out)[BINARY_SIZE]; + +static int max_threads; +static region_t *memory; + +static secp256k1_context *secp256k1_ctx; + +static void init(struct fmt_main *self) +{ + omp_autotune(self, OMP_SCALE); + +#ifdef _OPENMP + max_threads = omp_get_max_threads(); +#else + max_threads = 1; +#endif + + memory = mem_alloc(sizeof(*memory) * max_threads); + int i; + for (i = 0; i < max_threads; i++) + init_region_t(&memory[i]); + + saved_key = mem_calloc(self->params.max_keys_per_crypt, sizeof(*saved_key)); + crypt_out = mem_calloc(self->params.max_keys_per_crypt, sizeof(*crypt_out)); + + /* + * Use a shared context as permitted by comment in secp256k1.h for + * calls that take a const pointer. + */ + if (!(secp256k1_ctx = secp256k1_context_create(SECP256K1_CONTEXT_SIGN))) { + fprintf(stderr, "Failed to create secp256k1 context\n"); + error(); + } +} + +static void done(void) +{ + int i; + for (i = 0; i < max_threads; i++) + free_region_t(&memory[i]); + MEM_FREE(memory); + + MEM_FREE(saved_key); + MEM_FREE(crypt_out); + + secp256k1_context_destroy(secp256k1_ctx); +} + +static void *get_salt(char *ciphertext) +{ + uint8_t decoded[BINARY_SIZE + SALT_SIZE]; + int err; + + ciphertext += FORMAT_TAG_LEN; + if (base64_convert(ciphertext, e_b64_mime, strlen(ciphertext), + decoded, e_b64_raw, sizeof(decoded), flg_Base64_DONOT_NULL_TERMINATE, &err) != sizeof(decoded) || err) + return NULL; + + static struct custom_salt salt; + memcpy(&salt, &decoded[BINARY_SIZE], sizeof(salt)); +#if !ARCH_LITTLE_ENDIAN + salt.bytes_reqd = JOHNSWAP(salt.bytes_reqd); + salt.iter_count = JOHNSWAP(salt.iter_count); +#endif + + if (salt.bytes_reqd < 1024 || (salt.bytes_reqd & (salt.bytes_reqd - 1))) + return NULL; + + CRC32_t ctx; + unsigned char crc[4]; + CRC32_Init(&ctx); + CRC32_Update(&ctx, "ar:", 3); + CRC32_Update(&ctx, decoded, sizeof(decoded) - sizeof(salt.crc)); + CRC32_Final(crc, ctx); + if (memcmp(salt.crc, crc, sizeof(crc))) + return NULL; + + return &salt; +} + +static void *get_binary(char *ciphertext) +{ + static uint8_t binary[BINARY_SIZE]; + int err; + + ciphertext += FORMAT_TAG_LEN; + if (base64_convert(ciphertext, e_b64_mime, strlen(ciphertext), + binary, e_b64_raw, sizeof(binary), flg_Base64_DONOT_NULL_TERMINATE, &err) != sizeof(binary) || err) + return NULL; + + return binary; +} + +static int valid(char *ciphertext, struct fmt_main *self) +{ + if (strncmp(ciphertext, FORMAT_TAG, FORMAT_TAG_LEN) || strlen(ciphertext) != CIPHERTEXT_LENGTH) + return 0; + + return get_salt(ciphertext) != NULL; +} + +static void set_salt(void *salt) +{ + saved_salt = salt; +} + +static void set_key(char *key, int index) +{ + char *p = saved_key[index]; + *p = 0; + strncat(p, key, PLAINTEXT_LENGTH); +} + +static char *get_key(int index) +{ + return saved_key[index]; +} + +typedef union { + uint8_t u8[64]; + uint32_t u32[16]; + uint64_t u64[8]; +} lut_item; + +static int derive_key(region_t *memory, char *key, const struct custom_salt *salt, uint8_t *dk) +{ + lut_item *lut = memory->aligned; + + if (!lut || memory->aligned_size < salt->bytes_reqd) { + free_region_t(memory); + if (!(lut = alloc_region_t(memory, salt->bytes_reqd))) + return -1; + } + + uint8_t *mk = (uint8_t *)key; + size_t mklen = strlen(key); + + uint32_t n = salt->bytes_reqd >> 6; + uint32_t i = salt->iter_count; + do { + lut_item *p; + SHA512_CTX ctx; + + SHA512_Init(&ctx); + SHA512_Update(&ctx, mk, mklen); + SHA512_Update(&ctx, salt->seed, sizeof(salt->seed)); + SHA512_Final(lut[0].u8, &ctx); + + p = lut; + do { + SHA512_Init(&ctx); + SHA512_Update(&ctx, p, 64); + SHA512_Final((++p)->u8, &ctx); + } while (p < &lut[n - 1]); + + lut_item x = *p; + uint32_t j = n >> 1; + do { + uint32_t v = x.u32[15]; +#if !ARCH_LITTLE_ENDIAN + v = JOHNSWAP(v); +#endif + p = &lut[v & (n - 1)]; + + uint32_t k; + for (k = 0; k < 8; k++) + x.u64[k] ^= p->u64[k]; + + SHA512_Init(&ctx); + SHA512_Update(&ctx, &x, 64); + SHA512_Final(x.u8, &ctx); + } while (--j); + + memcpy(mk = dk, x.u8, mklen = 32); + } while (--i); + + return 0; +} + +static int derive_address(region_t *memory, char *key, const struct custom_salt *salt, uint8_t *da) +{ + union { + uint8_t u8[32]; + uint64_t u64[4]; + } dk; + + /* Derive AES key */ + if (derive_key(memory, key, salt, dk.u8)) + return -1; + + /* AES CFB mode decryption */ + AES_KEY ak; + AES_set_encrypt_key(dk.u8, 256, &ak); + AES_encrypt(saved_salt->iv, dk.u8, &ak); + uint64_t xor[2]; + memcpy(xor, saved_salt->privkey, sizeof(xor)); + dk.u64[0] ^= xor[0]; + dk.u64[1] ^= xor[1]; + AES_encrypt(saved_salt->privkey, &dk.u8[16], &ak); + memcpy(xor, saved_salt->privkey + 16, sizeof(xor)); + dk.u64[2] ^= xor[0]; + dk.u64[3] ^= xor[1]; + + /* Compute public key from decrypted private key */ + secp256k1_pubkey pubkey; + if (secp256k1_ec_pubkey_create(secp256k1_ctx, &pubkey, dk.u8)) { + unsigned char ser[65]; + size_t serlen = sizeof(ser); + secp256k1_ec_pubkey_serialize(secp256k1_ctx, ser, &serlen, &pubkey, SECP256K1_EC_UNCOMPRESSED); + + /* Hash160 */ + { + SHA256_CTX ctx; + SHA256_Init(&ctx); + SHA256_Update(&ctx, ser, sizeof(ser)); + SHA256_Final(dk.u8, &ctx); + } + { + sph_ripemd160_context ctx; + sph_ripemd160_init(&ctx); + sph_ripemd160(&ctx, dk.u8, 32); + sph_ripemd160_close(&ctx, da); + } + } else { + memset(da, 0, BINARY_SIZE); + } + + return 0; +} + +static int crypt_all(int *pcount, struct db_salt *salt) +{ + int failed = 0, count = *pcount, index; + +#ifdef _OPENMP +#pragma omp parallel for default(none) private(index) shared(count, failed, max_threads, memory, saved_key, saved_salt, crypt_out) +#endif + for (index = 0; index < count; index++) { +#ifdef _OPENMP + int t = omp_get_thread_num(); + if (t >= max_threads) { + failed = -1; + continue; + } +#else + const int t = 0; +#endif + errno = 0; + if (derive_address(&memory[t], saved_key[index], saved_salt, crypt_out[index])) { + failed = errno ? errno : ENOMEM; +#ifndef _OPENMP + break; +#endif + } + } + + if (failed) { +#ifdef _OPENMP + if (failed < 0) { + fprintf(stderr, "OpenMP thread number out of range\n"); + error(); + } +#endif + fprintf(stderr, "Memory allocation failed: %s\n", strerror(failed)); + error(); + } + + + return count; +} + +static int cmp_all(void *binary, int count) +{ + return 1; +} + +static int cmp_one(void *binary, int index) +{ + return !memcmp(binary, crypt_out[index], BINARY_SIZE); +} + +static int cmp_exact(char *source, int index) +{ + return 1; +} + +static unsigned int tunable_cost_memory(void *_salt) +{ + struct custom_salt *salt = (struct custom_salt *)_salt; + return salt->bytes_reqd; +} + +static unsigned int tunable_cost_iterations(void *_salt) +{ + struct custom_salt *salt = (struct custom_salt *)_salt; + return salt->iter_count; +} + +struct fmt_main fmt_armory = { + { + FORMAT_LABEL, + FORMAT_NAME, + ALGORITHM_NAME, + BENCHMARK_COMMENT, + BENCHMARK_LENGTH, + 0, + PLAINTEXT_LENGTH, + BINARY_SIZE, + BINARY_ALIGN, + SALT_SIZE, + SALT_ALIGN, + MIN_KEYS_PER_CRYPT, + MAX_KEYS_PER_CRYPT, + FMT_CASE | FMT_8_BIT | FMT_OMP, + {"memory", "iterations"}, + {FORMAT_TAG}, + tests + }, { + init, + done, + fmt_default_reset, + fmt_default_prepare, + valid, + fmt_default_split, + get_binary, + get_salt, + {tunable_cost_memory, tunable_cost_iterations}, + fmt_default_source, + {fmt_default_binary_hash}, + fmt_default_salt_hash, + NULL, + set_salt, + set_key, + get_key, + fmt_default_clear_keys, + crypt_all, + {fmt_default_get_hash}, + cmp_all, + cmp_one, + cmp_exact + } +}; + +#endif /* plugin stanza */