diff --git a/Makefile b/Makefile index 05611df..2eeff52 100644 --- a/Makefile +++ b/Makefile @@ -192,7 +192,8 @@ asm-tests-docker: # Boots a stock A500/68000 under Copperline, runs the RFC 4226 HOTP vectors on # real m68k, and checks the codes it emits over serial. See tests/copperline. # OTP core chain (hotp_sha1 -> hmac -> sha1) + the DRBG; no vault/prefs/front-end. -SERIALTEST_SRCS := src/core/otp.c src/core/hmac.c src/core/sha1.c src/core/drbg.c \ +SERIALTEST_SRCS := src/core/otp.c src/core/hmac.c src/core/sha1.c \ + src/core/sha256.c src/core/sha512.c src/core/drbg.c \ tests/copperline/serialtest.c serialtest-m68k: | $(BUILD) diff --git a/docs/VAULT_FORMAT.md b/docs/VAULT_FORMAT.md index 847949e..b4a6b10 100644 --- a/docs/VAULT_FORMAT.md +++ b/docs/VAULT_FORMAT.md @@ -132,7 +132,7 @@ always-unlocked) is the ordered account list: then account_count records, each: +------+------------------------------------------------------------+ | 1 | type (0 = TOTP, 1 = HOTP) | -| 1 | algorithm (0 = SHA1; 1/2 reserved for SHA256/512 in v2) | +| 1 | algorithm (0 = SHA1, 1 = SHA256, 2 = SHA512) | | 1 | digits (6 or 8) | | 4 | period (u32, seconds; TOTP) | | 8 | counter (u64; HOTP) | @@ -167,7 +167,8 @@ always-unlocked) is the ordered account list: 6. Recover the payload: `cipher_id = chacha20` → decrypt; `none` → payload is the ciphertext bytes. 7. Parse the payload into accounts. Any inconsistency (bad count, a length prefix - running past `payload_len`) → `VAULT_ERR_FORMAT`. + running past `payload_len`, or a per-record `algorithm` id the reader does + not implement) → `VAULT_ERR_FORMAT`. 8. Mark the vault unlocked; retain `salt`, `enc_key`, `mac_key` while resident. ## Save algorithm diff --git a/src/cli/main.c b/src/cli/main.c index 1bfcd27..b8de6f9 100644 --- a/src/cli/main.c +++ b/src/cli/main.c @@ -756,21 +756,24 @@ static int cmd_get(const char *path, const char *account) if (idx < 0) { vault_lock(&v); fprintf(stderr, "AmiAuth: no account matching '%s'\n", account); return 2; } a = &v.accounts[idx]; - if (strcmp(a->type, "hotp") == 0) { - printf("%0*lu\n", a->digits, - (unsigned long)hotp_sha1(a->secret, a->secret_len, a->counter, a->digits)); - a->counter++; /* HOTP is stateful: advance and persist */ - rc = save_vault(&v, path); - if (rc != VAULT_OK) - fprintf(stderr, "AmiAuth: warning: could not persist HOTP counter (%s)\n", - vault_err(rc)); - } else { - cli_clock_init(&clk); - now = clock_now_utc(&clk); - printf("%0*lu\n", a->digits, - (unsigned long)totp_sha1(a->secret, a->secret_len, now, 0, a->period, a->digits)); - fprintf(stderr, "(%u seconds remaining)\n", - totp_seconds_remaining(now, 0, a->period)); + { + char code[OTP_CODE_BUF]; + if (strcmp(a->type, "hotp") == 0) { + otp_render(a, 0, code); + printf("%s\n", code); + a->counter++; /* HOTP is stateful: advance and persist */ + rc = save_vault(&v, path); + if (rc != VAULT_OK) + fprintf(stderr, "AmiAuth: warning: could not persist HOTP counter (%s)\n", + vault_err(rc)); + } else { + cli_clock_init(&clk); + now = clock_now_utc(&clk); + otp_render(a, now, code); + printf("%s\n", code); + fprintf(stderr, "(%u seconds remaining)\n", + totp_seconds_remaining(now, 0, a->period)); + } } vault_lock(&v); return 0; diff --git a/src/core/hmac.c b/src/core/hmac.c index 37f637a..4335710 100644 --- a/src/core/hmac.c +++ b/src/core/hmac.c @@ -1,5 +1,7 @@ -/* hmac.c — HMAC-SHA1 (RFC 2104), one-shot and streaming. - * Validated against RFC 2202 vectors in tests/test_hmac.c. */ +/* hmac.c — HMAC (RFC 2104) over SHA-1/SHA-256/SHA-512, one-shot and streaming. + * Validated against RFC 2202 / RFC 4231 vectors in tests/test_hmac.c. + * The SHA-256/512 variants are line-for-line parallels of the SHA-1 one + * (see the note in hmac.h); keep the three in step when changing any. */ #include #include "hmac.h" @@ -59,3 +61,115 @@ void hmac_sha1(const uint8_t *key, size_t keylen, hmac_sha1_update(&ctx, msg, msglen); hmac_sha1_final(&ctx, out); } + +/* --- HMAC-SHA256 ----------------------------------------------------------- */ + +void hmac_sha256_init(hmac_sha256_ctx *ctx, const uint8_t *key, size_t keylen) +{ + uint8_t k[SHA256_BLOCK_SIZE]; + uint8_t ipad[SHA256_BLOCK_SIZE]; + size_t i; + + if (!ctx) return; + + memset(k, 0, sizeof(k)); + if (keylen > SHA256_BLOCK_SIZE) { + sha256(key, keylen, k); + } else if (keylen) { + memcpy(k, key, keylen); + } + + for (i = 0; i < SHA256_BLOCK_SIZE; i++) { + ipad[i] = k[i] ^ 0x36; + ctx->opad[i] = k[i] ^ 0x5c; + } + + sha256_init(&ctx->inner); + sha256_update(&ctx->inner, ipad, SHA256_BLOCK_SIZE); +} + +void hmac_sha256_update(hmac_sha256_ctx *ctx, const void *data, size_t len) +{ + if (!ctx) return; + sha256_update(&ctx->inner, data, len); +} + +void hmac_sha256_final(hmac_sha256_ctx *ctx, uint8_t out[SHA256_DIGEST_SIZE]) +{ + uint8_t inner[SHA256_DIGEST_SIZE]; + sha256_ctx outer; + + if (!ctx || !out) return; + + sha256_final(&ctx->inner, inner); + sha256_init(&outer); + sha256_update(&outer, ctx->opad, SHA256_BLOCK_SIZE); + sha256_update(&outer, inner, SHA256_DIGEST_SIZE); + sha256_final(&outer, out); +} + +void hmac_sha256(const uint8_t *key, size_t keylen, + const uint8_t *msg, size_t msglen, + uint8_t out[SHA256_DIGEST_SIZE]) +{ + hmac_sha256_ctx ctx; + hmac_sha256_init(&ctx, key, keylen); + hmac_sha256_update(&ctx, msg, msglen); + hmac_sha256_final(&ctx, out); +} + +/* --- HMAC-SHA512 ----------------------------------------------------------- */ + +void hmac_sha512_init(hmac_sha512_ctx *ctx, const uint8_t *key, size_t keylen) +{ + uint8_t k[SHA512_BLOCK_SIZE]; + uint8_t ipad[SHA512_BLOCK_SIZE]; + size_t i; + + if (!ctx) return; + + memset(k, 0, sizeof(k)); + if (keylen > SHA512_BLOCK_SIZE) { + sha512(key, keylen, k); + } else if (keylen) { + memcpy(k, key, keylen); + } + + for (i = 0; i < SHA512_BLOCK_SIZE; i++) { + ipad[i] = k[i] ^ 0x36; + ctx->opad[i] = k[i] ^ 0x5c; + } + + sha512_init(&ctx->inner); + sha512_update(&ctx->inner, ipad, SHA512_BLOCK_SIZE); +} + +void hmac_sha512_update(hmac_sha512_ctx *ctx, const void *data, size_t len) +{ + if (!ctx) return; + sha512_update(&ctx->inner, data, len); +} + +void hmac_sha512_final(hmac_sha512_ctx *ctx, uint8_t out[SHA512_DIGEST_SIZE]) +{ + uint8_t inner[SHA512_DIGEST_SIZE]; + sha512_ctx outer; + + if (!ctx || !out) return; + + sha512_final(&ctx->inner, inner); + sha512_init(&outer); + sha512_update(&outer, ctx->opad, SHA512_BLOCK_SIZE); + sha512_update(&outer, inner, SHA512_DIGEST_SIZE); + sha512_final(&outer, out); +} + +void hmac_sha512(const uint8_t *key, size_t keylen, + const uint8_t *msg, size_t msglen, + uint8_t out[SHA512_DIGEST_SIZE]) +{ + hmac_sha512_ctx ctx; + hmac_sha512_init(&ctx, key, keylen); + hmac_sha512_update(&ctx, msg, msglen); + hmac_sha512_final(&ctx, out); +} diff --git a/src/core/hmac.h b/src/core/hmac.h index 4dc223e..3eb6b31 100644 --- a/src/core/hmac.h +++ b/src/core/hmac.h @@ -1,5 +1,8 @@ -/* hmac.h — HMAC-SHA1 (RFC 2104), one-shot and streaming. - * Validated against RFC 2202 vectors in tests/test_hmac.c. */ +/* hmac.h — HMAC (RFC 2104) over SHA-1/SHA-256/SHA-512, one-shot and streaming. + * Validated against RFC 2202 / RFC 4231 vectors in tests/test_hmac.c. The three + * variants are deliberately parallel copies rather than one vtable-driven + * generic: each is ~50 lines, and the explicit forms keep call sites (PBKDF2, + * vault MAC, DRBG — all SHA-1) free of indirection. */ #ifndef AMIAUTH_HMAC_H #define AMIAUTH_HMAC_H @@ -7,8 +10,10 @@ #include #include "sha1.h" +#include "sha256.h" +#include "sha512.h" -/* Streaming context: the inner SHA-1 runs incrementally; opad is kept for the +/* Streaming context: the inner hash runs incrementally; opad is kept for the * outer pass in _final. */ typedef struct { sha1_ctx inner; @@ -24,4 +29,30 @@ void hmac_sha1(const uint8_t *key, size_t keylen, const uint8_t *msg, size_t msglen, uint8_t out[SHA1_DIGEST_SIZE]); +typedef struct { + sha256_ctx inner; + uint8_t opad[SHA256_BLOCK_SIZE]; +} hmac_sha256_ctx; + +void hmac_sha256_init(hmac_sha256_ctx *ctx, const uint8_t *key, size_t keylen); +void hmac_sha256_update(hmac_sha256_ctx *ctx, const void *data, size_t len); +void hmac_sha256_final(hmac_sha256_ctx *ctx, uint8_t out[SHA256_DIGEST_SIZE]); + +void hmac_sha256(const uint8_t *key, size_t keylen, + const uint8_t *msg, size_t msglen, + uint8_t out[SHA256_DIGEST_SIZE]); + +typedef struct { + sha512_ctx inner; + uint8_t opad[SHA512_BLOCK_SIZE]; +} hmac_sha512_ctx; + +void hmac_sha512_init(hmac_sha512_ctx *ctx, const uint8_t *key, size_t keylen); +void hmac_sha512_update(hmac_sha512_ctx *ctx, const void *data, size_t len); +void hmac_sha512_final(hmac_sha512_ctx *ctx, uint8_t out[SHA512_DIGEST_SIZE]); + +void hmac_sha512(const uint8_t *key, size_t keylen, + const uint8_t *msg, size_t msglen, + uint8_t out[SHA512_DIGEST_SIZE]); + #endif /* AMIAUTH_HMAC_H */ diff --git a/src/core/otp.c b/src/core/otp.c index 60f1a24..7e1f970 100644 --- a/src/core/otp.c +++ b/src/core/otp.c @@ -1,13 +1,43 @@ -/* otp.c — HOTP (RFC 4226) / TOTP (RFC 6238). +/* otp.c — HOTP (RFC 4226) / TOTP (RFC 6238), over SHA-1/SHA-256/SHA-512. * Validated against RFC 4226 App. D and RFC 6238 App. B (tests/test_otp.c). */ +#include + #include "otp.h" +#include "uri.h" #include "hmac.h" -uint32_t hotp_sha1(const uint8_t *key, size_t keylen, - uint64_t counter, int digits) +int otp_alg_from_name(const char *name) +{ + static const char *names[] = { "SHA1", "SHA256", "SHA512" }; + size_t i; + if (!name) return -1; + for (i = 0; i < sizeof(names) / sizeof(names[0]); i++) { + const char *a = name, *b = names[i]; + for (; *a && *b; a++, b++) { + int c = *a; + if (c >= 'a' && c <= 'z') c -= 32; + if (c != *b) break; + } + if (!*a && !*b) return (int)i; + } + return -1; +} + +const char *otp_alg_name(otp_alg alg) +{ + switch (alg) { + case OTP_ALG_SHA256: return "SHA256"; + case OTP_ALG_SHA512: return "SHA512"; + default: return "SHA1"; + } +} + +uint32_t hotp(otp_alg alg, const uint8_t *key, size_t keylen, + uint64_t counter, int digits) { uint8_t msg[8]; - uint8_t mac[SHA1_DIGEST_SIZE]; + uint8_t mac[SHA512_DIGEST_SIZE]; /* big enough for every variant */ + size_t maclen; uint32_t bin, mod; int i, offset; @@ -19,11 +49,24 @@ uint32_t hotp_sha1(const uint8_t *key, size_t keylen, counter >>= 8; } - hmac_sha1(key, keylen, msg, sizeof(msg), mac); + switch (alg) { + case OTP_ALG_SHA256: + hmac_sha256(key, keylen, msg, sizeof(msg), mac); + maclen = SHA256_DIGEST_SIZE; + break; + case OTP_ALG_SHA512: + hmac_sha512(key, keylen, msg, sizeof(msg), mac); + maclen = SHA512_DIGEST_SIZE; + break; + default: + hmac_sha1(key, keylen, msg, sizeof(msg), mac); + maclen = SHA1_DIGEST_SIZE; + break; + } /* Dynamic truncation (RFC 4226 §5.3): low nibble of the last byte selects a * 4-byte window; mask the high bit to stay positive. */ - offset = mac[SHA1_DIGEST_SIZE - 1] & 0x0f; + offset = mac[maclen - 1] & 0x0f; bin = ((uint32_t)(mac[offset] & 0x7f) << 24) | ((uint32_t)mac[offset + 1] << 16) | ((uint32_t)mac[offset + 2] << 8) @@ -34,14 +77,27 @@ uint32_t hotp_sha1(const uint8_t *key, size_t keylen, return bin % mod; } -uint32_t totp_sha1(const uint8_t *key, size_t keylen, - uint64_t unix_time, uint64_t t0, - uint32_t period, int digits) +uint32_t totp(otp_alg alg, const uint8_t *key, size_t keylen, + uint64_t unix_time, uint64_t t0, + uint32_t period, int digits) { uint64_t counter; if (period == 0) period = OTP_DEFAULT_PERIOD; counter = (unix_time - t0) / period; - return hotp_sha1(key, keylen, counter, digits); + return hotp(alg, key, keylen, counter, digits); +} + +uint32_t hotp_sha1(const uint8_t *key, size_t keylen, + uint64_t counter, int digits) +{ + return hotp(OTP_ALG_SHA1, key, keylen, counter, digits); +} + +uint32_t totp_sha1(const uint8_t *key, size_t keylen, + uint64_t unix_time, uint64_t t0, + uint32_t period, int digits) +{ + return totp(OTP_ALG_SHA1, key, keylen, unix_time, t0, period, digits); } uint32_t totp_seconds_remaining(uint64_t unix_time, uint64_t t0, uint32_t period) @@ -49,3 +105,29 @@ uint32_t totp_seconds_remaining(uint64_t unix_time, uint64_t t0, uint32_t period if (period == 0) period = OTP_DEFAULT_PERIOD; return (uint32_t)(period - ((unix_time - t0) % period)); } + +void otp_render(const struct otp_account *a, uint64_t unix_time, + char buf[OTP_CODE_BUF]) +{ + int alg = otp_alg_from_name(a->algorithm); + int digits = (a->digits >= 1 && a->digits <= 9) ? a->digits : OTP_DEFAULT_DIGITS; + uint32_t code; + int i; + + /* Both entry points (otpauth parse, vault load) reject algorithms we don't + * implement, so this is only a defensive fallback. */ + if (alg < 0) alg = OTP_ALG_SHA1; + + if (strcmp(a->type, "hotp") == 0) + code = hotp((otp_alg)alg, a->secret, a->secret_len, a->counter, digits); + else + code = totp((otp_alg)alg, a->secret, a->secret_len, unix_time, 0, + a->period, digits); + + /* Zero-padded by hand: libnix sprintf lacks '*' width. */ + buf[digits] = '\0'; + for (i = digits - 1; i >= 0; i--) { + buf[i] = (char)('0' + code % 10); + code /= 10; + } +} diff --git a/src/core/otp.h b/src/core/otp.h index 1edeeb0..f010b38 100644 --- a/src/core/otp.h +++ b/src/core/otp.h @@ -8,12 +8,34 @@ #define OTP_DEFAULT_DIGITS 6 #define OTP_DEFAULT_PERIOD 30 -/* HOTP: HMAC-SHA1 over an 8-byte counter, dynamic truncation, modulo 10^digits. +/* HMAC hash for code generation (RFC 6238 allows SHA-256/512 alongside SHA-1). + * The values are also the vault's on-disk `algorithm` ids — keep them in step + * with docs/VAULT_FORMAT.md. */ +typedef enum { + OTP_ALG_SHA1 = 0, + OTP_ALG_SHA256 = 1, + OTP_ALG_SHA512 = 2 +} otp_alg; + +/* Map an otpauth algorithm name (case-insensitive) to its otp_alg; -1 if it is + * not one we implement. The reverse always yields the canonical upper-case + * name (unknown values fall back to "SHA1"). */ +int otp_alg_from_name(const char *name); +const char *otp_alg_name(otp_alg alg); + +/* HOTP: HMAC over an 8-byte counter, dynamic truncation, modulo 10^digits. * Returns the code as an integer (zero-pad to `digits` when displaying). */ -uint32_t hotp_sha1(const uint8_t *key, size_t keylen, - uint64_t counter, int digits); +uint32_t hotp(otp_alg alg, const uint8_t *key, size_t keylen, + uint64_t counter, int digits); /* TOTP: HOTP with counter = (unix_time - t0) / period. */ +uint32_t totp(otp_alg alg, const uint8_t *key, size_t keylen, + uint64_t unix_time, uint64_t t0, + uint32_t period, int digits); + +/* SHA-1 shorthands (the overwhelmingly common variant). */ +uint32_t hotp_sha1(const uint8_t *key, size_t keylen, + uint64_t counter, int digits); uint32_t totp_sha1(const uint8_t *key, size_t keylen, uint64_t unix_time, uint64_t t0, uint32_t period, int digits); @@ -21,4 +43,13 @@ uint32_t totp_sha1(const uint8_t *key, size_t keylen, /* Seconds remaining in the current TOTP step for the given time. */ uint32_t totp_seconds_remaining(uint64_t unix_time, uint64_t t0, uint32_t period); +/* Render an account's current code as a display string: dispatches on the + * account's type and algorithm and zero-pads to its digit count. Uses + * a->counter for HOTP (advancing/persisting it stays the caller's job) and + * `unix_time` for TOTP. The one code path every front-end shares. */ +#define OTP_CODE_BUF 16 +struct otp_account; +void otp_render(const struct otp_account *a, uint64_t unix_time, + char buf[OTP_CODE_BUF]); + #endif /* AMIAUTH_OTP_H */ diff --git a/src/core/sha256.c b/src/core/sha256.c new file mode 100644 index 0000000..5840ca8 --- /dev/null +++ b/src/core/sha256.c @@ -0,0 +1,153 @@ +/* sha256.c — SHA-256 (FIPS 180-4). + * Validated against tests/test_sha256.c and the OpenSSL differential harness. + * Portable C only: unlike SHA-1 there is no asm/dispatch seam — OTP does one + * short HMAC per code, so this is never a hot loop (PBKDF2/vault stay SHA-1). */ +#include + +#include "sha256.h" + +#define ROTR32(x, n) (((x) >> (n)) | ((x) << (32 - (n)))) + +static const uint32_t K[64] = { + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, + 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, + 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, + 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, + 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, + 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, + 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, + 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, + 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 +}; + +static void sha256_compress(uint32_t state[8], const uint8_t block[SHA256_BLOCK_SIZE]) +{ + uint32_t w[64]; + uint32_t a, b, c, d, e, f, g, h; + int i; + + for (i = 0; i < 16; i++) { + w[i] = ((uint32_t)block[i * 4] << 24) + | ((uint32_t)block[i * 4 + 1] << 16) + | ((uint32_t)block[i * 4 + 2] << 8) + | ((uint32_t)block[i * 4 + 3]); + } + for (i = 16; i < 64; i++) { + uint32_t s0 = ROTR32(w[i - 15], 7) ^ ROTR32(w[i - 15], 18) ^ (w[i - 15] >> 3); + uint32_t s1 = ROTR32(w[i - 2], 17) ^ ROTR32(w[i - 2], 19) ^ (w[i - 2] >> 10); + w[i] = w[i - 16] + s0 + w[i - 7] + s1; + } + + a = state[0]; b = state[1]; c = state[2]; d = state[3]; + e = state[4]; f = state[5]; g = state[6]; h = state[7]; + + for (i = 0; i < 64; i++) { + uint32_t S1 = ROTR32(e, 6) ^ ROTR32(e, 11) ^ ROTR32(e, 25); + uint32_t ch = (e & f) ^ (~e & g); + uint32_t t1 = h + S1 + ch + K[i] + w[i]; + uint32_t S0 = ROTR32(a, 2) ^ ROTR32(a, 13) ^ ROTR32(a, 22); + uint32_t maj = (a & b) ^ (a & c) ^ (b & c); + uint32_t t2 = S0 + maj; + h = g; g = f; f = e; e = d + t1; + d = c; c = b; b = a; a = t1 + t2; + } + + state[0] += a; state[1] += b; state[2] += c; state[3] += d; + state[4] += e; state[5] += f; state[6] += g; state[7] += h; +} + +void sha256_init(sha256_ctx *ctx) +{ + if (!ctx) return; + ctx->state[0] = 0x6a09e667; + ctx->state[1] = 0xbb67ae85; + ctx->state[2] = 0x3c6ef372; + ctx->state[3] = 0xa54ff53a; + ctx->state[4] = 0x510e527f; + ctx->state[5] = 0x9b05688c; + ctx->state[6] = 0x1f83d9ab; + ctx->state[7] = 0x5be0cd19; + ctx->count = 0; + ctx->buflen = 0; +} + +void sha256_update(sha256_ctx *ctx, const void *data, size_t len) +{ + const uint8_t *p = (const uint8_t *)data; + if (!ctx || !p) return; + + ctx->count += len; + + /* Top up a partial block first. */ + if (ctx->buflen) { + size_t need = SHA256_BLOCK_SIZE - ctx->buflen; + size_t take = len < need ? len : need; + memcpy(ctx->buf + ctx->buflen, p, take); + ctx->buflen += take; + p += take; + len -= take; + if (ctx->buflen == SHA256_BLOCK_SIZE) { + sha256_compress(ctx->state, ctx->buf); + ctx->buflen = 0; + } + } + + /* Whole blocks straight from the input. */ + while (len >= SHA256_BLOCK_SIZE) { + sha256_compress(ctx->state, p); + p += SHA256_BLOCK_SIZE; + len -= SHA256_BLOCK_SIZE; + } + + /* Stash the remainder. */ + if (len) { + memcpy(ctx->buf, p, len); + ctx->buflen = len; + } +} + +void sha256_final(sha256_ctx *ctx, uint8_t out[SHA256_DIGEST_SIZE]) +{ + uint64_t bits; + int i; + + if (!ctx || !out) return; + bits = ctx->count * 8; + + /* Append 0x80, then zero-pad. If the length field won't fit in this block, + * finish the block and pad into a fresh one. */ + ctx->buf[ctx->buflen++] = 0x80; + if (ctx->buflen > 56) { + while (ctx->buflen < SHA256_BLOCK_SIZE) ctx->buf[ctx->buflen++] = 0; + sha256_compress(ctx->state, ctx->buf); + ctx->buflen = 0; + } + while (ctx->buflen < 56) ctx->buf[ctx->buflen++] = 0; + + /* 64-bit big-endian bit length. */ + for (i = 0; i < 8; i++) + ctx->buf[56 + i] = (uint8_t)(bits >> (56 - 8 * i)); + sha256_compress(ctx->state, ctx->buf); + + for (i = 0; i < 8; i++) { + out[i * 4] = (uint8_t)(ctx->state[i] >> 24); + out[i * 4 + 1] = (uint8_t)(ctx->state[i] >> 16); + out[i * 4 + 2] = (uint8_t)(ctx->state[i] >> 8); + out[i * 4 + 3] = (uint8_t)(ctx->state[i]); + } +} + +void sha256(const void *data, size_t len, uint8_t out[SHA256_DIGEST_SIZE]) +{ + sha256_ctx ctx; + sha256_init(&ctx); + sha256_update(&ctx, data, len); + sha256_final(&ctx, out); +} diff --git a/src/core/sha256.h b/src/core/sha256.h new file mode 100644 index 0000000..5a46037 --- /dev/null +++ b/src/core/sha256.h @@ -0,0 +1,25 @@ +/* sha256.h — SHA-256 (FIPS 180-4), for the RFC 6238 HMAC-SHA256 TOTP variant. */ +#ifndef AMIAUTH_SHA256_H +#define AMIAUTH_SHA256_H + +#include +#include + +#define SHA256_DIGEST_SIZE 32 +#define SHA256_BLOCK_SIZE 64 + +typedef struct { + uint32_t state[8]; + uint64_t count; /* total bytes processed */ + uint8_t buf[SHA256_BLOCK_SIZE]; + size_t buflen; /* bytes currently in buf */ +} sha256_ctx; + +void sha256_init(sha256_ctx *ctx); +void sha256_update(sha256_ctx *ctx, const void *data, size_t len); +void sha256_final(sha256_ctx *ctx, uint8_t out[SHA256_DIGEST_SIZE]); + +/* One-shot convenience wrapper. */ +void sha256(const void *data, size_t len, uint8_t out[SHA256_DIGEST_SIZE]); + +#endif /* AMIAUTH_SHA256_H */ diff --git a/src/core/sha512.c b/src/core/sha512.c new file mode 100644 index 0000000..31c7b82 --- /dev/null +++ b/src/core/sha512.c @@ -0,0 +1,162 @@ +/* sha512.c — SHA-512 (FIPS 180-4). + * Validated against tests/test_sha512.c and the OpenSSL differential harness. + * The 64-bit arithmetic compiles to libgcc helpers on the 68000; that is fine — + * OTP does one short HMAC per code, so this is never a hot loop. A 128-bit + * message length is not tracked: a single uint64_t byte count caps input at + * 2^61 bytes, far beyond anything this program can see. */ +#include + +#include "sha512.h" + +#define ROTR64(x, n) (((x) >> (n)) | ((x) << (64 - (n)))) + +static const uint64_t K[80] = { + 0x428a2f98d728ae22ULL, 0x7137449123ef65cdULL, 0xb5c0fbcfec4d3b2fULL, 0xe9b5dba58189dbbcULL, + 0x3956c25bf348b538ULL, 0x59f111f1b605d019ULL, 0x923f82a4af194f9bULL, 0xab1c5ed5da6d8118ULL, + 0xd807aa98a3030242ULL, 0x12835b0145706fbeULL, 0x243185be4ee4b28cULL, 0x550c7dc3d5ffb4e2ULL, + 0x72be5d74f27b896fULL, 0x80deb1fe3b1696b1ULL, 0x9bdc06a725c71235ULL, 0xc19bf174cf692694ULL, + 0xe49b69c19ef14ad2ULL, 0xefbe4786384f25e3ULL, 0x0fc19dc68b8cd5b5ULL, 0x240ca1cc77ac9c65ULL, + 0x2de92c6f592b0275ULL, 0x4a7484aa6ea6e483ULL, 0x5cb0a9dcbd41fbd4ULL, 0x76f988da831153b5ULL, + 0x983e5152ee66dfabULL, 0xa831c66d2db43210ULL, 0xb00327c898fb213fULL, 0xbf597fc7beef0ee4ULL, + 0xc6e00bf33da88fc2ULL, 0xd5a79147930aa725ULL, 0x06ca6351e003826fULL, 0x142929670a0e6e70ULL, + 0x27b70a8546d22ffcULL, 0x2e1b21385c26c926ULL, 0x4d2c6dfc5ac42aedULL, 0x53380d139d95b3dfULL, + 0x650a73548baf63deULL, 0x766a0abb3c77b2a8ULL, 0x81c2c92e47edaee6ULL, 0x92722c851482353bULL, + 0xa2bfe8a14cf10364ULL, 0xa81a664bbc423001ULL, 0xc24b8b70d0f89791ULL, 0xc76c51a30654be30ULL, + 0xd192e819d6ef5218ULL, 0xd69906245565a910ULL, 0xf40e35855771202aULL, 0x106aa07032bbd1b8ULL, + 0x19a4c116b8d2d0c8ULL, 0x1e376c085141ab53ULL, 0x2748774cdf8eeb99ULL, 0x34b0bcb5e19b48a8ULL, + 0x391c0cb3c5c95a63ULL, 0x4ed8aa4ae3418acbULL, 0x5b9cca4f7763e373ULL, 0x682e6ff3d6b2b8a3ULL, + 0x748f82ee5defb2fcULL, 0x78a5636f43172f60ULL, 0x84c87814a1f0ab72ULL, 0x8cc702081a6439ecULL, + 0x90befffa23631e28ULL, 0xa4506cebde82bde9ULL, 0xbef9a3f7b2c67915ULL, 0xc67178f2e372532bULL, + 0xca273eceea26619cULL, 0xd186b8c721c0c207ULL, 0xeada7dd6cde0eb1eULL, 0xf57d4f7fee6ed178ULL, + 0x06f067aa72176fbaULL, 0x0a637dc5a2c898a6ULL, 0x113f9804bef90daeULL, 0x1b710b35131c471bULL, + 0x28db77f523047d84ULL, 0x32caab7b40c72493ULL, 0x3c9ebe0a15c9bebcULL, 0x431d67c49c100d4cULL, + 0x4cc5d4becb3e42b6ULL, 0x597f299cfc657e2aULL, 0x5fcb6fab3ad6faecULL, 0x6c44198c4a475817ULL +}; + +static void sha512_compress(uint64_t state[8], const uint8_t block[SHA512_BLOCK_SIZE]) +{ + uint64_t w[80]; + uint64_t a, b, c, d, e, f, g, h; + int i; + + for (i = 0; i < 16; i++) { + w[i] = ((uint64_t)block[i * 8] << 56) + | ((uint64_t)block[i * 8 + 1] << 48) + | ((uint64_t)block[i * 8 + 2] << 40) + | ((uint64_t)block[i * 8 + 3] << 32) + | ((uint64_t)block[i * 8 + 4] << 24) + | ((uint64_t)block[i * 8 + 5] << 16) + | ((uint64_t)block[i * 8 + 6] << 8) + | ((uint64_t)block[i * 8 + 7]); + } + for (i = 16; i < 80; i++) { + uint64_t s0 = ROTR64(w[i - 15], 1) ^ ROTR64(w[i - 15], 8) ^ (w[i - 15] >> 7); + uint64_t s1 = ROTR64(w[i - 2], 19) ^ ROTR64(w[i - 2], 61) ^ (w[i - 2] >> 6); + w[i] = w[i - 16] + s0 + w[i - 7] + s1; + } + + a = state[0]; b = state[1]; c = state[2]; d = state[3]; + e = state[4]; f = state[5]; g = state[6]; h = state[7]; + + for (i = 0; i < 80; i++) { + uint64_t S1 = ROTR64(e, 14) ^ ROTR64(e, 18) ^ ROTR64(e, 41); + uint64_t ch = (e & f) ^ (~e & g); + uint64_t t1 = h + S1 + ch + K[i] + w[i]; + uint64_t S0 = ROTR64(a, 28) ^ ROTR64(a, 34) ^ ROTR64(a, 39); + uint64_t maj = (a & b) ^ (a & c) ^ (b & c); + uint64_t t2 = S0 + maj; + h = g; g = f; f = e; e = d + t1; + d = c; c = b; b = a; a = t1 + t2; + } + + state[0] += a; state[1] += b; state[2] += c; state[3] += d; + state[4] += e; state[5] += f; state[6] += g; state[7] += h; +} + +void sha512_init(sha512_ctx *ctx) +{ + if (!ctx) return; + ctx->state[0] = 0x6a09e667f3bcc908ULL; + ctx->state[1] = 0xbb67ae8584caa73bULL; + ctx->state[2] = 0x3c6ef372fe94f82bULL; + ctx->state[3] = 0xa54ff53a5f1d36f1ULL; + ctx->state[4] = 0x510e527fade682d1ULL; + ctx->state[5] = 0x9b05688c2b3e6c1fULL; + ctx->state[6] = 0x1f83d9abfb41bd6bULL; + ctx->state[7] = 0x5be0cd19137e2179ULL; + ctx->count = 0; + ctx->buflen = 0; +} + +void sha512_update(sha512_ctx *ctx, const void *data, size_t len) +{ + const uint8_t *p = (const uint8_t *)data; + if (!ctx || !p) return; + + ctx->count += len; + + /* Top up a partial block first. */ + if (ctx->buflen) { + size_t need = SHA512_BLOCK_SIZE - ctx->buflen; + size_t take = len < need ? len : need; + memcpy(ctx->buf + ctx->buflen, p, take); + ctx->buflen += take; + p += take; + len -= take; + if (ctx->buflen == SHA512_BLOCK_SIZE) { + sha512_compress(ctx->state, ctx->buf); + ctx->buflen = 0; + } + } + + /* Whole blocks straight from the input. */ + while (len >= SHA512_BLOCK_SIZE) { + sha512_compress(ctx->state, p); + p += SHA512_BLOCK_SIZE; + len -= SHA512_BLOCK_SIZE; + } + + /* Stash the remainder. */ + if (len) { + memcpy(ctx->buf, p, len); + ctx->buflen = len; + } +} + +void sha512_final(sha512_ctx *ctx, uint8_t out[SHA512_DIGEST_SIZE]) +{ + uint64_t bits; + int i; + + if (!ctx || !out) return; + bits = ctx->count * 8; + + /* Append 0x80, then zero-pad. The length field occupies the final 16 bytes + * (the high 8 are always zero here — see the 2^61 note above). */ + ctx->buf[ctx->buflen++] = 0x80; + if (ctx->buflen > 112) { + while (ctx->buflen < SHA512_BLOCK_SIZE) ctx->buf[ctx->buflen++] = 0; + sha512_compress(ctx->state, ctx->buf); + ctx->buflen = 0; + } + while (ctx->buflen < 120) ctx->buf[ctx->buflen++] = 0; + + /* 64-bit big-endian bit length (low half of the 128-bit field). */ + for (i = 0; i < 8; i++) + ctx->buf[120 + i] = (uint8_t)(bits >> (56 - 8 * i)); + sha512_compress(ctx->state, ctx->buf); + + for (i = 0; i < 8; i++) { + int j; + for (j = 0; j < 8; j++) + out[i * 8 + j] = (uint8_t)(ctx->state[i] >> (56 - 8 * j)); + } +} + +void sha512(const void *data, size_t len, uint8_t out[SHA512_DIGEST_SIZE]) +{ + sha512_ctx ctx; + sha512_init(&ctx); + sha512_update(&ctx, data, len); + sha512_final(&ctx, out); +} diff --git a/src/core/sha512.h b/src/core/sha512.h new file mode 100644 index 0000000..a794927 --- /dev/null +++ b/src/core/sha512.h @@ -0,0 +1,25 @@ +/* sha512.h — SHA-512 (FIPS 180-4), for the RFC 6238 HMAC-SHA512 TOTP variant. */ +#ifndef AMIAUTH_SHA512_H +#define AMIAUTH_SHA512_H + +#include +#include + +#define SHA512_DIGEST_SIZE 64 +#define SHA512_BLOCK_SIZE 128 + +typedef struct { + uint64_t state[8]; + uint64_t count; /* total bytes processed */ + uint8_t buf[SHA512_BLOCK_SIZE]; + size_t buflen; /* bytes currently in buf */ +} sha512_ctx; + +void sha512_init(sha512_ctx *ctx); +void sha512_update(sha512_ctx *ctx, const void *data, size_t len); +void sha512_final(sha512_ctx *ctx, uint8_t out[SHA512_DIGEST_SIZE]); + +/* One-shot convenience wrapper. */ +void sha512(const void *data, size_t len, uint8_t out[SHA512_DIGEST_SIZE]); + +#endif /* AMIAUTH_SHA512_H */ diff --git a/src/core/uri.c b/src/core/uri.c index 044936e..e9b7475 100644 --- a/src/core/uri.c +++ b/src/core/uri.c @@ -145,7 +145,11 @@ int otpauth_parse(const char *uri, otp_account *out) } else if (key_is(q, klen, "issuer")) { copy_str(out->issuer, sizeof(out->issuer), dec); } else if (key_is(q, klen, "algorithm")) { - copy_str(out->algorithm, sizeof(out->algorithm), dec); + /* Only SHA-1/256/512 exist (RFC 6238); refuse anything + * else rather than silently mint wrong codes with SHA-1. */ + int alg = otp_alg_from_name(dec); + if (alg < 0) return -1; + strcpy(out->algorithm, otp_alg_name((otp_alg)alg)); } else if (key_is(q, klen, "digits")) { int d = atoi(dec); if (d == 6 || d == 8) out->digits = d; diff --git a/src/core/uri.h b/src/core/uri.h index 6212f10..af1d812 100644 --- a/src/core/uri.h +++ b/src/core/uri.h @@ -9,10 +9,11 @@ #define OTP_MAX_ISSUER 64 #define OTP_MAX_LABEL 128 -/* A single OTP account. This is the unit stored in the vault. */ -typedef struct { +/* A single OTP account. This is the unit stored in the vault. (The struct tag + * lets otp.h's renderer take a pointer without pulling in this header.) */ +typedef struct otp_account { char type[8]; /* "totp" or "hotp" */ - char algorithm[8]; /* "SHA1" (v1); SHA256/512 are v2 */ + char algorithm[8]; /* "SHA1", "SHA256" or "SHA512" */ char issuer[OTP_MAX_ISSUER]; char label[OTP_MAX_LABEL]; uint8_t secret[OTP_MAX_SECRET]; /* raw key bytes (Base32-decoded) */ diff --git a/src/core/vault.c b/src/core/vault.c index 665fa93..87e82e7 100644 --- a/src/core/vault.c +++ b/src/core/vault.c @@ -12,6 +12,7 @@ #include "vault.h" #include "chacha20.h" +#include "otp.h" #include "pbkdf2.h" #include "hmac.h" @@ -128,8 +129,9 @@ static size_t serialize_payload(const vault *v, uint8_t *out) size_t il = strlen(a->issuer); size_t ll = strlen(a->label); + int alg = otp_alg_from_name(a->algorithm); out[p++] = (uint8_t)(strcmp(a->type, "hotp") == 0 ? 1 : 0); - out[p++] = 0; /* algorithm: SHA1 (v1) */ + out[p++] = (uint8_t)(alg > 0 ? alg : 0); /* 0/1/2 = SHA1/256/512 */ out[p++] = (uint8_t)a->digits; put_u32(out + p, a->period); p += 4; put_u64(out + p, a->counter); p += 8; @@ -157,17 +159,21 @@ static vault_result parse_payload(vault *v, const uint8_t *in, size_t len) v->count = 0; for (i = 0; i < count; i++) { otp_account a; - uint8_t type, sl, il, ll; + uint8_t type, alg, sl, il, ll; memset(&a, 0, sizeof(a)); if (p + 15 > len) return VAULT_ERR_FORMAT; /* fixed fields */ type = in[p++]; - p++; /* algorithm (SHA1 in v1) */ + alg = in[p++]; a.digits = in[p++]; a.period = get_u32(in + p); p += 4; a.counter = get_u64(in + p); p += 8; + /* Reject algorithm ids we don't implement: guessing would produce + * plausible-looking but wrong codes (the format doc says new ids + * extend the scheme and old readers must refuse them). */ + if (alg > OTP_ALG_SHA512) return VAULT_ERR_FORMAT; strcpy(a.type, type ? "hotp" : "totp"); - strcpy(a.algorithm, "SHA1"); + strcpy(a.algorithm, otp_alg_name((otp_alg)alg)); if (p + 1 > len) return VAULT_ERR_FORMAT; sl = in[p++]; diff --git a/src/gui/main.c b/src/gui/main.c index 3f7c917..756d9ac 100644 --- a/src/gui/main.c +++ b/src/gui/main.c @@ -1925,17 +1925,15 @@ int main(int argc, char **argv) if (idx < 0) { req->aar_Result = AAR_NOTFOUND; break; } { otp_account *a = &v.accounts[idx]; - char fmt[8]; - unsigned long code; + char code[OTP_CODE_BUF]; if (strcmp(a->type, "hotp") == 0) { - code = (unsigned long)hotp_sha1(a->secret, a->secret_len, a->counter, a->digits); + otp_render(a, 0, code); a->counter++; /* stateful: persist */ if (gui_save(&v, path) != VAULT_OK) req->aar_Result = AAR_SAVEFAIL; } else { - uint64_t now = clock_now_utc(&clk); - code = (unsigned long)totp_sha1(a->secret, a->secret_len, now, 0, a->period, a->digits); + otp_render(a, clock_now_utc(&clk), code); } - if (rb && rbcap) { sprintf(fmt, "%%0%dlu\n", (int)a->digits); sprintf(rb, fmt, code); } + if (rb && rbcap > strlen(code) + 1) { strcpy(rb, code); strcat(rb, "\n"); } } break; } @@ -2228,17 +2226,17 @@ int main(int argc, char **argv) if (have_timer && (sigs & timersig) && win && running && v.count > 0) { uint64_t now = clock_now_utc(&clk); uint32_t sel_period = OTP_DEFAULT_PERIOD, sel_rem = 0; - char fmt[8], newcode[sizeof g_code[0]]; + char newcode[OTP_CODE_BUF]; int list_changed = 0; if (sel >= v.count) sel = 0; for (i = 0; i < v.count; i++) { const otp_account *a = &v.accounts[i]; uint32_t period = a->period ? a->period : OTP_DEFAULT_PERIOD; - uint32_t code = totp_sha1(a->secret, a->secret_len, now, 0, period, a->digits); uint32_t rem = totp_seconds_remaining(now, 0, period); - /* libnix sprintf lacks '*' width, so build "%06lu"/"%08lu". */ - sprintf(fmt, "%%0%dlu", (int)a->digits); - sprintf(newcode, fmt, (unsigned long)code); + /* Dispatches on the account's type and algorithm; for HOTP the + * row shows the code at the current counter (it changes when a + * GET advances the counter, not with the clock). */ + otp_render(a, now, newcode); /* A full LISTBROWSER_Labels reset stalls unrelated Workbench * drag gestures system-wide (#62 - closed-source ReAction * cost, not fixable on our side), so only pay for it when a diff --git a/tests/diff/diff_main.c b/tests/diff/diff_main.c index 0b86e1c..adeaa2e 100644 --- a/tests/diff/diff_main.c +++ b/tests/diff/diff_main.c @@ -20,6 +20,8 @@ #include #include "sha1.h" +#include "sha256.h" +#include "sha512.h" #include "hmac.h" #include "chacha20.h" #include "pbkdf2.h" @@ -81,6 +83,44 @@ static void diff_sha1(int iters) } } +static void diff_sha256(int iters) +{ + uint8_t buf[4096], a[32], b[32]; + unsigned int blen; + int i; + for (i = 0; i < iters; i++) { + size_t n = rng_len(sizeof(buf)); + rng_bytes(buf, n); + sha256(buf, n, a); + EVP_Digest(buf, n, b, &blen, EVP_sha256(), NULL); + g_checks++; + if (memcmp(a, b, 32) != 0) { + char d[64]; + sprintf(d, "len=%lu", (unsigned long)n); + report_fail("sha256", d); + } + } +} + +static void diff_sha512(int iters) +{ + uint8_t buf[4096], a[64], b[64]; + unsigned int blen; + int i; + for (i = 0; i < iters; i++) { + size_t n = rng_len(sizeof(buf)); + rng_bytes(buf, n); + sha512(buf, n, a); + EVP_Digest(buf, n, b, &blen, EVP_sha512(), NULL); + g_checks++; + if (memcmp(a, b, 64) != 0) { + char d[64]; + sprintf(d, "len=%lu", (unsigned long)n); + report_fail("sha512", d); + } + } +} + static void diff_hmac(int iters) { uint8_t key[128], msg[4096], a[20], b[20]; @@ -97,7 +137,49 @@ static void diff_hmac(int iters) if (memcmp(a, b, 20) != 0) { char d[64]; sprintf(d, "keylen=%lu msglen=%lu", (unsigned long)kl, (unsigned long)ml); - report_fail("hmac", d); + report_fail("hmac-sha1", d); + } + } +} + +static void diff_hmac_sha256(int iters) +{ + uint8_t key[137], msg[4096], a[32], b[32]; + unsigned int blen; + int i; + for (i = 0; i < iters; i++) { + size_t kl = rng_len(sizeof(key)); /* spans <, =, > the 64-byte block */ + size_t ml = rng_len(sizeof(msg)); + rng_bytes(key, kl); + rng_bytes(msg, ml); + hmac_sha256(key, kl, msg, ml, a); + HMAC(EVP_sha256(), key, (int)kl, msg, ml, b, &blen); + g_checks++; + if (memcmp(a, b, 32) != 0) { + char d[64]; + sprintf(d, "keylen=%lu msglen=%lu", (unsigned long)kl, (unsigned long)ml); + report_fail("hmac-sha256", d); + } + } +} + +static void diff_hmac_sha512(int iters) +{ + uint8_t key[137], msg[4096], a[64], b[64]; + unsigned int blen; + int i; + for (i = 0; i < iters; i++) { + size_t kl = rng_len(sizeof(key)); /* spans <, =, > the 128-byte block */ + size_t ml = rng_len(sizeof(msg)); + rng_bytes(key, kl); + rng_bytes(msg, ml); + hmac_sha512(key, kl, msg, ml, a); + HMAC(EVP_sha512(), key, (int)kl, msg, ml, b, &blen); + g_checks++; + if (memcmp(a, b, 64) != 0) { + char d[64]; + sprintf(d, "keylen=%lu msglen=%lu", (unsigned long)kl, (unsigned long)ml); + report_fail("hmac-sha512", d); } } } @@ -177,7 +259,11 @@ int main(int argc, char **argv) iters, (unsigned long long)seed); diff_sha1(iters); + diff_sha256(iters); + diff_sha512(iters); diff_hmac(iters); + diff_hmac_sha256(iters); + diff_hmac_sha512(iters); diff_chacha20(iters); diff_pbkdf2(iters); diff --git a/tests/test_hmac.c b/tests/test_hmac.c index b053780..5176f9c 100644 --- a/tests/test_hmac.c +++ b/tests/test_hmac.c @@ -1,4 +1,4 @@ -/* test_hmac.c — HMAC-SHA1 vectors (RFC 2202). */ +/* test_hmac.c — HMAC-SHA1 vectors (RFC 2202) and HMAC-SHA256/512 (RFC 4231). */ #include #include "test.h" @@ -6,30 +6,76 @@ void run_hmac_tests(void) { - uint8_t mac[SHA1_DIGEST_SIZE]; - uint8_t key[80]; + uint8_t mac[SHA512_DIGEST_SIZE]; + uint8_t key[131]; uint8_t data[50]; /* RFC 2202 case 1: key = 0x0b x20, data = "Hi There". */ memset(key, 0x0b, 20); hmac_sha1(key, 20, (const uint8_t *)"Hi There", 8, mac); - TEST_CHECK(hex_eq(mac, sizeof(mac), "b617318655057264e28bc0b6fb378c8ef146be00")); + TEST_CHECK(hex_eq(mac, SHA1_DIGEST_SIZE, "b617318655057264e28bc0b6fb378c8ef146be00")); /* RFC 2202 case 2: key = "Jefe" (shorter than block). */ hmac_sha1((const uint8_t *)"Jefe", 4, (const uint8_t *)"what do ya want for nothing?", 28, mac); - TEST_CHECK(hex_eq(mac, sizeof(mac), "effcdf6ae5eb2fa2d27416d5f184df9c259a7c79")); + TEST_CHECK(hex_eq(mac, SHA1_DIGEST_SIZE, "effcdf6ae5eb2fa2d27416d5f184df9c259a7c79")); /* RFC 2202 case 3: key = 0xaa x20, data = 0xdd x50. */ memset(key, 0xaa, 20); memset(data, 0xdd, 50); hmac_sha1(key, 20, data, 50, mac); - TEST_CHECK(hex_eq(mac, sizeof(mac), "125d7342b9ac11cd91a39af48aa17b4f63f175d3")); + TEST_CHECK(hex_eq(mac, SHA1_DIGEST_SIZE, "125d7342b9ac11cd91a39af48aa17b4f63f175d3")); /* RFC 2202 case 6: key = 0xaa x80 (longer than block -> hashed first). */ memset(key, 0xaa, 80); hmac_sha1(key, 80, (const uint8_t *)"Test Using Larger Than Block-Size Key - Hash Key First", 54, mac); - TEST_CHECK(hex_eq(mac, sizeof(mac), "aa4ae5e15272d00e95705637ce8a3b55ed402112")); + TEST_CHECK(hex_eq(mac, SHA1_DIGEST_SIZE, "aa4ae5e15272d00e95705637ce8a3b55ed402112")); + + /* RFC 4231 case 1: key = 0x0b x20, data = "Hi There". */ + memset(key, 0x0b, 20); + hmac_sha256(key, 20, (const uint8_t *)"Hi There", 8, mac); + TEST_CHECK(hex_eq(mac, SHA256_DIGEST_SIZE, + "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7")); + hmac_sha512(key, 20, (const uint8_t *)"Hi There", 8, mac); + TEST_CHECK(hex_eq(mac, SHA512_DIGEST_SIZE, + "87aa7cdea5ef619d4ff0b4241a1d6cb02379f4e2ce4ec2787ad0b30545e17cde" + "daa833b7d6b8a702038b274eaea3f4e4be9d914eeb61f1702e696c203a126854")); + + /* RFC 4231 case 2: key = "Jefe" (shorter than block). */ + hmac_sha256((const uint8_t *)"Jefe", 4, + (const uint8_t *)"what do ya want for nothing?", 28, mac); + TEST_CHECK(hex_eq(mac, SHA256_DIGEST_SIZE, + "5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843")); + hmac_sha512((const uint8_t *)"Jefe", 4, + (const uint8_t *)"what do ya want for nothing?", 28, mac); + TEST_CHECK(hex_eq(mac, SHA512_DIGEST_SIZE, + "164b7a7bfcf819e2e395fbe73b56e0a387bd64222e831fd610270cd7ea250554" + "9758bf75c05a994a6d034f65f8f0e6fdcaeab1a34d4a6b4b636e070a38bce737")); + + /* RFC 4231 case 3: key = 0xaa x20, data = 0xdd x50. */ + memset(key, 0xaa, 20); + memset(data, 0xdd, 50); + hmac_sha256(key, 20, data, 50, mac); + TEST_CHECK(hex_eq(mac, SHA256_DIGEST_SIZE, + "773ea91e36800e46854db8ebd09181a72959098b3ef8c122d9635514ced565fe")); + hmac_sha512(key, 20, data, 50, mac); + TEST_CHECK(hex_eq(mac, SHA512_DIGEST_SIZE, + "fa73b0089d56a284efb0f0756c890be9b1b5dbdd8ee81a3655f83e33b2279d39" + "bf3e848279a722c806b485a47e67c807b946a337bee8942674278859e13292fb")); + + /* RFC 4231 case 6: key = 0xaa x131 (longer than even the SHA-512 block). */ + memset(key, 0xaa, 131); + hmac_sha256(key, 131, + (const uint8_t *)"Test Using Larger Than Block-Size Key - Hash Key First", + 54, mac); + TEST_CHECK(hex_eq(mac, SHA256_DIGEST_SIZE, + "60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54")); + hmac_sha512(key, 131, + (const uint8_t *)"Test Using Larger Than Block-Size Key - Hash Key First", + 54, mac); + TEST_CHECK(hex_eq(mac, SHA512_DIGEST_SIZE, + "80b24263c7c1a3ebb71493c1dd7be8b49b46d1f41b4aeec1121b013783f8f352" + "6b56d037e05f2598bd0fd2215d6a1e5295e64f73f63f0aec8b915a985d786598")); } diff --git a/tests/test_main.c b/tests/test_main.c index 7cf88c6..5240dad 100644 --- a/tests/test_main.c +++ b/tests/test_main.c @@ -4,6 +4,8 @@ test_ctx g_test = { 0, 0, 0 }; void run_sha1_tests(void); +void run_sha256_tests(void); +void run_sha512_tests(void); void run_hmac_tests(void); void run_otp_tests(void); void run_base32_tests(void); @@ -21,6 +23,8 @@ void run_qr_tests(void); int main(void) { run_sha1_tests(); + run_sha256_tests(); + run_sha512_tests(); run_hmac_tests(); run_otp_tests(); run_base32_tests(); diff --git a/tests/test_otp.c b/tests/test_otp.c index 48de79b..b130ff7 100644 --- a/tests/test_otp.c +++ b/tests/test_otp.c @@ -1,12 +1,20 @@ /* test_otp.c — HOTP (RFC 4226 App. D) and TOTP (RFC 6238 App. B) vectors. */ +#include + #include "test.h" #include "otp.h" +#include "uri.h" void run_otp_tests(void) { - /* Both RFCs use the ASCII secret "12345678901234567890" (20 bytes). */ + /* Both RFCs use the ASCII secret "12345678901234567890" (20 bytes); the + * RFC 6238 SHA-256/512 vectors extend it to the hash's digest length. */ const uint8_t *secret = (const uint8_t *)"12345678901234567890"; const size_t slen = 20; + const uint8_t *secret32 = (const uint8_t *) + "12345678901234567890123456789012"; + const uint8_t *secret64 = (const uint8_t *) + "1234567890123456789012345678901234567890123456789012345678901234"; /* RFC 4226 App. D — HOTP, 6 digits, counters 0..9. */ static const uint32_t hotp_expected[10] = { @@ -26,6 +34,36 @@ void run_otp_tests(void) TEST_CHECK(totp_sha1(secret, slen, 2000000000ULL, 0, 30, 8) == 69279037u); TEST_CHECK(totp_sha1(secret, slen, 20000000000ULL, 0, 30, 8) == 65353130u); + /* RFC 6238 App. B — TOTP (SHA-256), 8 digits, 32-byte seed. */ + TEST_CHECK(totp(OTP_ALG_SHA256, secret32, 32, 59ULL, 0, 30, 8) == 46119246u); + TEST_CHECK(totp(OTP_ALG_SHA256, secret32, 32, 1111111109ULL, 0, 30, 8) == 68084774u); + TEST_CHECK(totp(OTP_ALG_SHA256, secret32, 32, 1111111111ULL, 0, 30, 8) == 67062674u); + TEST_CHECK(totp(OTP_ALG_SHA256, secret32, 32, 1234567890ULL, 0, 30, 8) == 91819424u); + TEST_CHECK(totp(OTP_ALG_SHA256, secret32, 32, 2000000000ULL, 0, 30, 8) == 90698825u); + TEST_CHECK(totp(OTP_ALG_SHA256, secret32, 32, 20000000000ULL, 0, 30, 8) == 77737706u); + + /* RFC 6238 App. B — TOTP (SHA-512), 8 digits, 64-byte seed. */ + TEST_CHECK(totp(OTP_ALG_SHA512, secret64, 64, 59ULL, 0, 30, 8) == 90693936u); + TEST_CHECK(totp(OTP_ALG_SHA512, secret64, 64, 1111111109ULL, 0, 30, 8) == 25091201u); + TEST_CHECK(totp(OTP_ALG_SHA512, secret64, 64, 1111111111ULL, 0, 30, 8) == 99943326u); + TEST_CHECK(totp(OTP_ALG_SHA512, secret64, 64, 1234567890ULL, 0, 30, 8) == 93441116u); + TEST_CHECK(totp(OTP_ALG_SHA512, secret64, 64, 2000000000ULL, 0, 30, 8) == 38618901u); + TEST_CHECK(totp(OTP_ALG_SHA512, secret64, 64, 20000000000ULL, 0, 30, 8) == 47863826u); + + /* The generic entry point with OTP_ALG_SHA1 matches the sha1 shorthand. */ + TEST_CHECK(totp(OTP_ALG_SHA1, secret, slen, 59ULL, 0, 30, 8) == 94287082u); + TEST_CHECK(hotp(OTP_ALG_SHA1, secret, slen, 0, 6) == 755224u); + + /* Algorithm-name mapping (case-insensitive; unknown rejected). */ + TEST_CHECK(otp_alg_from_name("SHA1") == OTP_ALG_SHA1); + TEST_CHECK(otp_alg_from_name("sha256") == OTP_ALG_SHA256); + TEST_CHECK(otp_alg_from_name("Sha512") == OTP_ALG_SHA512); + TEST_CHECK(otp_alg_from_name("SHA224") == -1); + TEST_CHECK(otp_alg_from_name("SHA25") == -1); + TEST_CHECK(otp_alg_from_name("") == -1); + TEST_CHECK(otp_alg_from_name(NULL) == -1); + TEST_CHECK(strcmp(otp_alg_name(OTP_ALG_SHA256), "SHA256") == 0); + /* totp_seconds_remaining is pure arithmetic and already implemented. */ TEST_CHECK(totp_seconds_remaining(0, 0, 30) == 30); TEST_CHECK(totp_seconds_remaining(1, 0, 30) == 29); @@ -34,4 +72,44 @@ void run_otp_tests(void) TEST_CHECK(totp_seconds_remaining(45, 0, 30) == 15); /* period==0 falls back to the 30s default. */ TEST_CHECK(totp_seconds_remaining(5, 0, 0) == 25); + + /* otp_render: the account-level dispatch + zero-padded formatting every + * front-end shares. Reuses the RFC vectors above. */ + { + otp_account a; + char buf[OTP_CODE_BUF]; + + memset(&a, 0, sizeof(a)); + strcpy(a.type, "totp"); + strcpy(a.algorithm, "SHA1"); + memcpy(a.secret, secret, slen); + a.secret_len = slen; + a.digits = 8; + a.period = 30; + otp_render(&a, 1111111109ULL, buf); + TEST_CHECK(strcmp(buf, "07081804") == 0); /* keeps the leading zero */ + + strcpy(a.algorithm, "SHA256"); + memcpy(a.secret, secret32, 32); + a.secret_len = 32; + otp_render(&a, 59ULL, buf); + TEST_CHECK(strcmp(buf, "46119246") == 0); + + strcpy(a.algorithm, "SHA512"); + memcpy(a.secret, secret64, 64); + a.secret_len = 64; + otp_render(&a, 59ULL, buf); + TEST_CHECK(strcmp(buf, "90693936") == 0); + + /* HOTP renders from the stored counter; the time argument is unused. */ + memset(&a, 0, sizeof(a)); + strcpy(a.type, "hotp"); + strcpy(a.algorithm, "SHA1"); + memcpy(a.secret, secret, slen); + a.secret_len = slen; + a.digits = 6; + a.counter = 5; + otp_render(&a, 999999ULL, buf); + TEST_CHECK(strcmp(buf, "254676") == 0); + } } diff --git a/tests/test_sha256.c b/tests/test_sha256.c new file mode 100644 index 0000000..c936391 --- /dev/null +++ b/tests/test_sha256.c @@ -0,0 +1,46 @@ +/* test_sha256.c — SHA-256 vectors (FIPS 180-4 / NIST examples). */ +#include + +#include "test.h" +#include "sha256.h" + +void run_sha256_tests(void) +{ + uint8_t out[SHA256_DIGEST_SIZE]; + sha256_ctx ctx; + int i; + + /* Empty message. */ + sha256((const uint8_t *)"", 0, out); + TEST_CHECK(hex_eq(out, sizeof(out), + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")); + + /* "abc" (FIPS 180-4 example 1). */ + sha256((const uint8_t *)"abc", 3, out); + TEST_CHECK(hex_eq(out, sizeof(out), + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad")); + + /* Two-block message (FIPS 180-4 example 2). */ + sha256((const uint8_t *) + "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", 56, out); + TEST_CHECK(hex_eq(out, sizeof(out), + "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1")); + + /* One million 'a', streamed in awkward chunk sizes to exercise buffering. */ + sha256_init(&ctx); + for (i = 0; i < 10000; i++) + sha256_update(&ctx, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaa", 100); + sha256_final(&ctx, out); + TEST_CHECK(hex_eq(out, sizeof(out), + "cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0")); + + /* Streaming across the block boundary equals the one-shot result. */ + sha256_init(&ctx); + sha256_update(&ctx, "abcdbcdecdefdefgefghfghighijhijk", 32); + sha256_update(&ctx, "ijkljklmklmnlmnomnopnopq", 24); + sha256_final(&ctx, out); + TEST_CHECK(hex_eq(out, sizeof(out), + "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1")); +} diff --git a/tests/test_sha512.c b/tests/test_sha512.c new file mode 100644 index 0000000..8ff4c0b --- /dev/null +++ b/tests/test_sha512.c @@ -0,0 +1,53 @@ +/* test_sha512.c — SHA-512 vectors (FIPS 180-4 / NIST examples). */ +#include + +#include "test.h" +#include "sha512.h" + +void run_sha512_tests(void) +{ + uint8_t out[SHA512_DIGEST_SIZE]; + sha512_ctx ctx; + int i; + + /* Empty message. */ + sha512((const uint8_t *)"", 0, out); + TEST_CHECK(hex_eq(out, sizeof(out), + "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce" + "47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e")); + + /* "abc" (FIPS 180-4 example 1). */ + sha512((const uint8_t *)"abc", 3, out); + TEST_CHECK(hex_eq(out, sizeof(out), + "ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a" + "2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f")); + + /* Two-block message (FIPS 180-4 example 2, the 896-bit message). */ + sha512((const uint8_t *) + "abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmno" + "ijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu", 112, out); + TEST_CHECK(hex_eq(out, sizeof(out), + "8e959b75dae313da8cf4f72814fc143f8f7779c6eb9f7fa17299aeadb6889018" + "501d289e4900f7e4331b99dec4b5433ac7d329eeb6dd26545e96e55b874be909")); + + /* One million 'a', streamed in awkward chunk sizes to exercise buffering. */ + sha512_init(&ctx); + for (i = 0; i < 10000; i++) + sha512_update(&ctx, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaa", 100); + sha512_final(&ctx, out); + TEST_CHECK(hex_eq(out, sizeof(out), + "e718483d0ce769644e2e42c7bc15b4638e1f98b13b2044285632a803afa973eb" + "de0ff244877ea60a4cb0432ce577c31beb009c5c2c49aa2e4eadb217ad8cc09b")); + + /* Streaming across the 128-byte block boundary equals the one-shot result. */ + sha512_init(&ctx); + sha512_update(&ctx, "abcdefghbcdefghicdefghijdefghijkefghijklfghijklm" + "ghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrs", 96); + sha512_update(&ctx, "mnopqrstnopqrstu", 16); + sha512_final(&ctx, out); + TEST_CHECK(hex_eq(out, sizeof(out), + "8e959b75dae313da8cf4f72814fc143f8f7779c6eb9f7fa17299aeadb6889018" + "501d289e4900f7e4331b99dec4b5433ac7d329eeb6dd26545e96e55b874be909")); +} diff --git a/tests/test_uri.c b/tests/test_uri.c index 28bb1cf..85e513b 100644 --- a/tests/test_uri.c +++ b/tests/test_uri.c @@ -45,6 +45,24 @@ void run_uri_tests(void) "otpauth://totp/OldName:acct?secret=JBSWY3DP&issuer=NewName", &a) == 0); TEST_CHECK(strcmp(a.issuer, "NewName") == 0); + /* algorithm= parses case-insensitively to the canonical name. */ + TEST_CHECK(otpauth_parse( + "otpauth://totp/x?secret=JBSWY3DP&algorithm=SHA256", &a) == 0); + TEST_CHECK(strcmp(a.algorithm, "SHA256") == 0); + TEST_CHECK(otpauth_parse( + "otpauth://totp/x?secret=JBSWY3DP&algorithm=sha512", &a) == 0); + TEST_CHECK(strcmp(a.algorithm, "SHA512") == 0); + TEST_CHECK(otpauth_parse( + "otpauth://totp/x?secret=JBSWY3DP&algorithm=Sha1", &a) == 0); + TEST_CHECK(strcmp(a.algorithm, "SHA1") == 0); + + /* An algorithm we don't implement is rejected outright — importing it and + * generating SHA-1 codes would be silently wrong. */ + TEST_CHECK(otpauth_parse( + "otpauth://totp/x?secret=JBSWY3DP&algorithm=MD5", &a) == -1); + TEST_CHECK(otpauth_parse( + "otpauth://totp/x?secret=JBSWY3DP&algorithm=SHA224", &a) == -1); + /* Rejections: missing secret, wrong scheme, malformed input. */ TEST_CHECK(otpauth_parse("otpauth://totp/x?issuer=Y", &a) == -1); TEST_CHECK(otpauth_parse("otpauth://weird/x?secret=JBSWY3DP", &a) == -1); diff --git a/tests/test_vault.c b/tests/test_vault.c index e44efb6..59001b6 100644 --- a/tests/test_vault.c +++ b/tests/test_vault.c @@ -8,6 +8,7 @@ #include "test.h" #include "vault.h" +#include "hmac.h" static const char *tmp_path(void) { @@ -42,6 +43,7 @@ static void sample_account(otp_account *a) static int accounts_equal(const otp_account *x, const otp_account *y) { return strcmp(x->type, y->type) == 0 + && strcmp(x->algorithm, y->algorithm) == 0 && strcmp(x->issuer, y->issuer) == 0 && strcmp(x->label, y->label) == 0 && x->secret_len == y->secret_len @@ -115,6 +117,68 @@ void run_vault_tests(void) TEST_CHECK(vault_load(&w, path, NULL) == VAULT_OK); TEST_CHECK(w.count == 1 && accounts_equal(&w.accounts[0], &a)); + /* --- SHA-256/512 accounts round-trip via algorithm ids 1/2 (#43) --- */ + { + otp_account b, c; + sample_account(&b); + strcpy(b.algorithm, "SHA256"); + sample_account(&c); + strcpy(c.algorithm, "SHA512"); + strcpy(c.label, "bob@example.com"); + + TEST_CHECK(vault_create(&v, NULL, 0, NULL) == VAULT_OK); + TEST_CHECK(vault_add(&v, &b) == VAULT_OK); + TEST_CHECK(vault_add(&v, &c) == VAULT_OK); + TEST_CHECK(vault_save(&v, path, NULL) == VAULT_OK); + TEST_CHECK(vault_load(&w, path, NULL) == VAULT_OK); + TEST_CHECK(w.count == 2); + TEST_CHECK(accounts_equal(&w.accounts[0], &b)); + TEST_CHECK(accounts_equal(&w.accounts[1], &c)); + } + + /* --- an algorithm id we don't implement is refused (format rule) --- */ + { + /* Hand-build a minimal always-unlocked vault whose one record claims + * algorithm id 9, with a valid (empty-key) MAC so only the id check + * can reject it. Payload: count=1; type 0, alg 9, digits 6, + * period 30, counter 0, secret "K", no issuer, label "x". */ + static const uint8_t payload[] = { + 0x00, 0x01, + 0x00, 0x09, 0x06, + 0x00, 0x00, 0x00, 0x1e, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 'K', + 0x00, + 0x01, 'x' + }; + uint8_t header[44]; + uint8_t mac[20]; + FILE *f; + + memset(header, 0, sizeof(header)); + memcpy(header, "AAVT", 4); + header[4] = 0x01; /* format_version */ + header[43] = (uint8_t)sizeof(payload); /* payload_len (u32 BE) */ + + { + hmac_sha1_ctx m; + hmac_sha1_init(&m, NULL, 0); + hmac_sha1_update(&m, header, sizeof(header)); + hmac_sha1_update(&m, payload, sizeof(payload)); + hmac_sha1_final(&m, mac); + } + + f = fopen(path, "wb"); + TEST_CHECK(f != NULL); + if (f) { + fwrite(header, 1, sizeof(header), f); + fwrite(mac, 1, sizeof(mac), f); + fwrite(payload, 1, sizeof(payload), f); + fclose(f); + } + TEST_CHECK(vault_load(&w, path, NULL) == VAULT_ERR_FORMAT); + } + /* --- encrypted lock clears secrets --- */ TEST_CHECK(vault_create(&v, "pw", 1024, SALT) == VAULT_OK); TEST_CHECK(vault_add(&v, &a) == VAULT_OK); diff --git a/userdocs/Managing-Accounts.md b/userdocs/Managing-Accounts.md index 6c9ae17..132319d 100644 --- a/userdocs/Managing-Accounts.md +++ b/userdocs/Managing-Accounts.md @@ -24,8 +24,10 @@ AmiAuth accepts all three forms. use *Account → Add from clipboard*. The URI's issuer, label, secret, algorithm, digits, period (TOTP) or counter -(HOTP) are all imported. AmiAuth v1 supports SHA-1 (the near-universal -default), 6 or 8 digits, and any period. +(HOTP) are all imported. AmiAuth supports SHA-1 (the near-universal default), +SHA-256 and SHA-512 (`algorithm=SHA256`/`SHA512` in the URI — rare, but some +services offer it), 6 or 8 digits, and any period. A URI naming an algorithm +AmiAuth doesn't implement is rejected rather than silently imported as SHA-1. ### From a bare Base32 secret diff --git a/userdocs/Security-Model.md b/userdocs/Security-Model.md index 54fcc91..a16cf7c 100644 --- a/userdocs/Security-Model.md +++ b/userdocs/Security-Model.md @@ -121,8 +121,9 @@ general secret vault. ## Verifying the crypto yourself -- All primitives (SHA-1, HMAC, PBKDF2, ChaCha20) and both OTP algorithms are - validated against their published RFC test vectors in CI on every commit. +- All primitives (SHA-1, SHA-256, SHA-512, HMAC, PBKDF2, ChaCha20) and both + OTP modes (TOTP/HOTP) across all three hash algorithms are validated + against their published RFC test vectors in CI on every commit. - An opt-in CI job differentially fuzzes the primitives against OpenSSL. - The full source is BSD-licensed: [github.com/sidick/amiauth](https://github.com/sidick/amiauth). The frozen diff --git a/userdocs/Troubleshooting-and-FAQ.md b/userdocs/Troubleshooting-and-FAQ.md index bad0880..29a61cd 100644 --- a/userdocs/Troubleshooting-and-FAQ.md +++ b/userdocs/Troubleshooting-and-FAQ.md @@ -145,14 +145,17 @@ Vault: `PROGDIR:AmiAuth.vault` (one file — copy it to back up). Settings: The data is unrecoverable — that is the point of the encryption. Re-enrol each account using the services' recovery codes or a second enrolled device. -**Steam Guard? SHA-256 codes? Exporting QR codes?** +**Steam Guard? Exporting QR codes?** On the v2 candidate list, along with an ARexx port and translations. See the -[v2 milestone](https://github.com/sidick/amiauth/milestone/2). +[v2 milestone](https://github.com/sidick/amiauth/milestone/2). (SHA-256/512 +TOTP is already supported — see [Managing Accounts](Managing-Accounts.md).) **Why SHA-1? Isn't that broken?** SHA-1's collision attacks do not affect HMAC-SHA1 as used by TOTP/HOTP (it -remains the algorithm virtually every service issues secrets for), nor the -vault's HMAC/PBKDF2 constructions. See [Security Model](Security-Model.md). +remains the algorithm virtually every service issues secrets for, and the one +the vault itself uses for its HMAC/PBKDF2 constructions), and AmiAuth also +supports SHA-256/SHA-512 for the rare service that issues those instead. See +[Security Model](Security-Model.md). **Was this really written by an AI?** Largely, yes — under human direction, review and on-hardware testing; it's