carlos / contra Public

Clone
git clone https://amadan.net/carlos/contra

Plain git — no account needed to clone.

Download

Download this file

1// Package crypto implements CONTRA's key hierarchy exactly as PROTOCOL.md
2// specifies it, and nothing else.
3//
4// Every derivation here is pinned by vectors/contra-v1.json. That file was
5// generated from the spec by an independent implementation — OpenSSL, driven
6// by vectors/gen/gen.c — and never from this code, so the test proves
7// agreement rather than self-consistency.
8//
9// Changing a value this package produces is changing the protocol. The vector
10// file, the JS console and the Swift core all have to move together, or a Mac
11// unlocks a vault the browser cannot.
12package crypto
13
14import (
15 "crypto/hkdf"
16 "crypto/hmac"
17 "crypto/sha256"
18
19 "golang.org/x/crypto/argon2"
20)
21
22// The versioned HKDF labels. Every derivation in the protocol carries one, so
23// that holding the output of one branch says nothing about any other.
24const (
25 LabelLogin = "contra/v1/login"
26 LabelKEK = "contra/v1/kek"
27 LabelRCLogin = "contra/v1/rc-login"
28 LabelRCKEK = "contra/v1/rc-kek"
29 LabelPRFKEK = "contra/v1/prf-kek"
30 LabelKCV = "contra/v1/kcv"
31 LabelECP256 = "contra/v1/ec-p256"
32 LabelAAD = "contra/v1/aad"
33 LabelECIES = "contra/v1/ecies"
34)
35
36// Argon2id parameters, from PROTOCOL.md.
37//
38// 64 MiB is the largest allocation that reliably succeeds in iOS Safari's WASM
39// heap under memory pressure. Lanes is 1 because Argon2id's output depends on
40// it and WASM without SharedArrayBuffer cannot parallelise — p > 1 would break
41// byte-identity between Go, JS and Swift, which is the whole point.
42const (
43 Argon2MemoryKiB uint32 = 64 * 1024
44 Argon2Time uint32 = 3
45 Argon2Lanes uint8 = 1
46 Argon2TagLen uint32 = 32
47)
48
49// StretchPassphrase is the one expensive call on the passphrase branch: S_pw.
50func StretchPassphrase(passphrase string, salt []byte) []byte {
51 return argon2.IDKey([]byte(passphrase), salt,
52 Argon2Time, Argon2MemoryKiB, Argon2Lanes, Argon2TagLen)
53}
54
55// Derive is HKDF-SHA-256, RFC 5869 extract-then-expand — the single mode this
56// protocol uses. A nil or empty salt is a zero-length salt, which RFC 5869
57// turns into HashLen zero bytes.
58func Derive(ikm, salt []byte, label string, n int) ([]byte, error) {
59 return hkdf.Key(sha256.New, ikm, salt, label, n)
60}
61
62// KCV is the 16-byte key-check value carried by every wrap row.
63//
64// AES-GCM is not key-committing, so this is the commitment check. It also
65// distinguishes "wrong key" from "corrupted blob", which is the distinction
66// every unlock path needs constantly — and it is how a passkey provider
67// returning a mismatched PRF is recognised as "fast unlock unavailable"
68// rather than reported as an authentication failure.
69func KCV(kek []byte) []byte {
70 m := hmac.New(sha256.New, kek)
71 m.Write([]byte(LabelKCV))
72 return m.Sum(nil)[:16]
73}
74