Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 3 additions & 2 deletions docs/VAULT_FORMAT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down Expand Up @@ -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
Expand Down
33 changes: 18 additions & 15 deletions src/cli/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
118 changes: 116 additions & 2 deletions src/core/hmac.c
Original file line number Diff line number Diff line change
@@ -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 <string.h>

#include "hmac.h"
Expand Down Expand Up @@ -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);
}
37 changes: 34 additions & 3 deletions src/core/hmac.h
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
/* 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

#include <stddef.h>
#include <stdint.h>

#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;
Expand All @@ -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 */
102 changes: 92 additions & 10 deletions src/core/otp.c
Original file line number Diff line number Diff line change
@@ -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 <string.h>

#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;

Expand All @@ -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)
Expand All @@ -34,18 +77,57 @@ 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)
{
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;
}
}
Loading