Flash 655 lines
// sha256: the kernel crypto unit — SHA-256, HMAC-SHA256, PBKDF2-HMAC-SHA256.
//
// Password-hashing primitives for FlashOS user authentication.
// The kernel hashes and salts credentials itself (the KDF lives in one
// audited place); userspace never needs these primitives, so this is an
// ordinary kernel module, not an ABI-shared one.
//
// Pure compute — no MMIO, no externs, no allocation, no std. Every working
// buffer is caller-stack or value-returned, so calling any function here
// can never perturb the free-page baseline the harness asserts. Host tests
// at the bottom gate the implementation against published vectors (NIST
// FIPS 180-2, RFC 4231, the standard PBKDF2-HMAC-SHA256 set): a wrong
// round constant or a flipped byte order is invisible at runtime — it
// would silently produce stable-but-wrong hashes, which is an
// authentication bypass or a permanently locked-out device. No consumer
// of these functions ships until the vector tests pass.
//
// Implementation notes, deliberate choices:
//
// - The incremental hasher (init/update/final) is the core; the one-shot
// helpers wrap it. Large inputs stream through a single 64-byte block
// buffer — nothing here ever needs a message-sized buffer.
// - Message words and the length field are assembled big-endian BY HAND
// (shifts + #truncate). AArch64 is little-endian and std.mem is not
// available freestanding; getting this wrong is the classic
// silently-wrong-digest bug, which is exactly what the NIST vectors
// catch.
// - Block fills are explicit byte loops, never #memcpy of a runtime
// length (project byte-loop discipline; see the FAT32 sub-sector write
// rule in DOCUMENTATION.md).
// - Addition is +% throughout the compression function: SHA-256 is
// defined over mod-2^32 arithmetic.
// ---- SHA-256 core (FIPS 180-4) ----
pub const DIGEST_LENGTH usize = 32
pub const BLOCK_LENGTH usize = 64
// Initial hash state: the first 32 bits of the fractional parts of the
// square roots of the first 8 primes.
const H0 = [8]u32{
0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A,
0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19
}
// Round constants: the first 32 bits of the fractional parts of the cube
// roots of the first 64 primes.
const K = [64]u32{
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
}
// Rotate-right for u32. n is comptime (all SHA-256 rotations are fixed),
// so both shift amounts are comptime-checked to fit the u5 shift type.
inline fn rotr(x u32, comptime n comptime_int) u32 {
return (x >> n) | (x << (32 - n))
}
// One 64-byte block through the compression function, updating `state`.
fn compress(state *mut [8]u32, block *[64]u8) void {
// Message schedule. W[0..15] are the block words read big-endian;
// W[16..63] extend them.
var w [64]u32 = undefined
var t usize = 0
while (t < 16) {
w[t] = (#as(u32, block[t * 4]) << 24) |
(#as(u32, block[t * 4 + 1]) << 16) |
(#as(u32, block[t * 4 + 2]) << 8) |
#as(u32, block[t * 4 + 3])
t += 1
}
while (t < 64) {
const s0 = rotr(w[t - 15], 7) ^ rotr(w[t - 15], 18) ^ (w[t - 15] >> 3)
const s1 = rotr(w[t - 2], 17) ^ rotr(w[t - 2], 19) ^ (w[t - 2] >> 10)
w[t] = w[t - 16] +% s0 +% w[t - 7] +% s1
t += 1
}
var a = state[0]
var b = state[1]
var c = state[2]
var d = state[3]
var e = state[4]
var f = state[5]
var g = state[6]
var h = state[7]
var i usize = 0
while (i < 64) {
const sum1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)
const ch = (e & f) ^ (~e & g)
const temp1 = h +% sum1 +% ch +% K[i] +% w[i]
const sum0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)
const maj = (a & b) ^ (a & c) ^ (b & c)
const temp2 = sum0 +% maj
h = g
g = f
f = e
e = d +% temp1
d = c
c = b
b = a
a = temp1 +% temp2
i += 1
}
state[0] +%= a
state[1] +%= b
state[2] +%= c
state[3] +%= d
state[4] +%= e
state[5] +%= f
state[6] +%= g
state[7] +%= h
}
// Incremental SHA-256. Usage: var h = Sha256.init(); h.update(...); ...;
// const digest = h.final(); — `final` consumes the state (padding is
// written into the block buffer); do not update() after final().
pub const Sha256 = struct {
state [8]u32,
block [64]u8,
block_len usize, // bytes pending in `block`, always < 64
total_len u64, // total message bytes absorbed (for the length field)
pub fn init() Sha256 {
return .{
.state = H0,
.block = .{0} ** 64,
.block_len = 0,
.total_len = 0
}
}
pub fn update(self *mut Sha256, data []u8) void {
// Byte loop into the block buffer; compress on every full block.
var i usize = 0
while (i < data.len) {
self.block[self.block_len] = data[i]
self.block_len += 1
if (self.block_len == 64) {
compress(&self.state, &self.block)
self.block_len = 0
}
i += 1
}
self.total_len +%= data.len
}
pub fn final(self *mut Sha256) [32]u8 {
const bit_len u64 = self.total_len *% 8
// Padding: a single 0x80 byte, zeros, then the 64-bit big-endian
// bit length closing out a block.
self.block[self.block_len] = 0x80
self.block_len += 1
if (self.block_len > 56) {
// No room for the length field — pad this block out first.
while (self.block_len < 64) {
self.block[self.block_len] = 0
self.block_len += 1
}
compress(&self.state, &self.block)
self.block_len = 0
}
while (self.block_len < 56) {
self.block[self.block_len] = 0
self.block_len += 1
}
self.block[56] = #truncate(bit_len >> 56)
self.block[57] = #truncate(bit_len >> 48)
self.block[58] = #truncate(bit_len >> 40)
self.block[59] = #truncate(bit_len >> 32)
self.block[60] = #truncate(bit_len >> 24)
self.block[61] = #truncate(bit_len >> 16)
self.block[62] = #truncate(bit_len >> 8)
self.block[63] = #truncate(bit_len)
compress(&self.state, &self.block)
// Serialize the state big-endian into the digest.
var out [32]u8 = undefined
var k usize = 0
while (k < 8) {
out[k * 4] = #truncate(self.state[k] >> 24)
out[k * 4 + 1] = #truncate(self.state[k] >> 16)
out[k * 4 + 2] = #truncate(self.state[k] >> 8)
out[k * 4 + 3] = #truncate(self.state[k])
k += 1
}
return out
}
}
// One-shot SHA-256.
pub fn sha256(msg []u8) [32]u8 {
var h = Sha256.init()
h.update(msg)
return h.final()
}
// ---- HMAC-SHA256 (RFC 2104) ----
// Keyed MAC with precomputed key pads: init() absorbs (key ^ ipad) and
// (key ^ opad) once, so every mac() afterwards costs two compressions.
// That is what makes PBKDF2's inner loop affordable — its iteration count
// times two compressions is the whole cost.
pub const HmacSha256 = struct {
inner_init Sha256, // state after absorbing (key ^ ipad)
outer_init Sha256, // state after absorbing (key ^ opad)
pub fn init(key []u8) HmacSha256 {
// Keys longer than the block are hashed first (RFC 2104); shorter
// keys are zero-padded to the block length.
var key_block [64]u8 = .{0} ** 64
if (key.len > 64) {
const kh = sha256(key)
var i usize = 0
while (i < 32) {
key_block[i] = kh[i]
i += 1
}
} else {
var i usize = 0
while (i < key.len) {
key_block[i] = key[i]
i += 1
}
}
var ipad [64]u8 = undefined
var opad [64]u8 = undefined
var i usize = 0
while (i < 64) {
ipad[i] = key_block[i] ^ 0x36
opad[i] = key_block[i] ^ 0x5C
i += 1
}
var inner = Sha256.init()
inner.update(ipad[0..])
var outer = Sha256.init()
outer.update(opad[0..])
return .{ .inner_init = inner, .outer_init = outer }
}
// Close an inner hash that was seeded from inner_init and fed message
// data: HMAC = H(opad || H(ipad || msg)).
pub fn finish(self *HmacSha256, inner *mut Sha256) [32]u8 {
const inner_digest = inner.final()
var outer = self.outer_init
outer.update(inner_digest[0..])
return outer.final()
}
// One-shot MAC over `msg` under this key.
pub fn mac(self *HmacSha256, msg []u8) [32]u8 {
var inner = self.inner_init
inner.update(msg)
return self.finish(&inner)
}
}
// One-shot HMAC-SHA256.
pub fn hmacSha256(key []u8, msg []u8) [32]u8 {
const ctx = HmacSha256.init(key)
return ctx.mac(msg)
}
// ---- PBKDF2-HMAC-SHA256 (RFC 2898 / RFC 8018) ----
// Derive out.len bytes of key material from a password and salt.
// iterations must be >= 1 (the caller picks the work factor; 0 would
// silently produce U_1-only material and is treated as 1).
pub fn pbkdf2HmacSha256(password []u8, salt []u8, iterations u32, out []mut u8) void {
const prf = HmacSha256.init(password)
const iters u32 = if (iterations == 0) 1 else iterations
var block_index u32 = 1 // T-block counter, 1-based per the RFC
var out_pos usize = 0
while (out_pos < out.len) {
// U_1 = PRF(P, S || INT_BE(block_index))
var inner = prf.inner_init
inner.update(salt)
const index_be = [4]u8{
#truncate(block_index >> 24),
#truncate(block_index >> 16),
#truncate(block_index >> 8),
#truncate(block_index)
}
inner.update(index_be[0..])
var u = prf.finish(&inner)
// T_i = U_1 ^ U_2 ^ ... ^ U_c, with U_j = PRF(P, U_{j-1}).
var t_block = u
var iter u32 = 1
while (iter < iters) {
u = prf.mac(u[0..])
var k usize = 0
while (k < 32) {
t_block[k] ^= u[k]
k += 1
}
iter += 1
}
// Emit min(32, remaining) bytes of T_i.
var n = out.len - out_pos
if (n > 32) { n = 32 }
var j usize = 0
while (j < n) {
out[out_pos + j] = t_block[j]
j += 1
}
out_pos += n
block_index +%= 1
}
}
// ---- Constant-time comparison ----
// Branch-free byte-slice equality for comparing secrets (a freshly derived
// PBKDF2 key against the stored verifier). Running time depends only on the
// slice length, not on the position of the first mismatch, so it leaks no
// information about a partially-correct guess. The length check
// short-circuits, but the compared lengths (fixed digest sizes) are public.
pub fn ctEql(a []u8, b []u8) bool {
if (a.len != b.len) { return false }
var diff u8 = 0
// Paired iteration over two equal-length slices, written as an index
// walk (Flash `for` binds a single sequence; the lengths are checked
// equal just above, so a[i]/b[i] visit the same byte pairs).
var i usize = 0
while (i < a.len) {
diff |= a[i] ^ b[i]
i += 1
}
return diff == 0
}
// ---- Host tests ----
//
// The vector tests below are the gate described in the header. Expected
// values are written as hex strings (decoded via std.fmt at test time) so
// they can be compared character-for-character against the published
// sources; the implementation itself never parses hex.
const std = #import("std")
const testing = std.testing
test "ctEql: equal slices" {
try testing.expect(ctEql("abc", "abc"))
try testing.expect(ctEql("", ""))
const k = [_]u8{ 0xDE, 0xAD, 0xBE, 0xEF }
try testing.expect(ctEql(k[0..], k[0..]))
}
test "ctEql: one-bit difference" {
try testing.expect(!ctEql("abc", "abd"))
try testing.expect(!ctEql(&[_]u8{0x00}, &[_]u8{0x01}))
}
test "ctEql: length mismatch" {
try testing.expect(!ctEql("abc", "ab"))
try testing.expect(!ctEql("", "a"))
}
fn expectDigestHex(comptime hex []u8, digest []u8) !void {
var expected [64]u8 = undefined
const bytes = try std.fmt.hexToBytes(expected[0 .. hex.len / 2], hex)
try testing.expectEqualSlices(u8, bytes, digest)
}
test "NIST FIPS 180-2: empty message" {
const d = sha256("")
try expectDigestHex(
"E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",
d[0..]
)
}
test "NIST FIPS 180-2: 'abc'" {
const d = sha256("abc")
try expectDigestHex(
"BA7816BF8F01CFEA414140DE5DAE2223B00361A396177A9CB410FF61F20015AD",
d[0..]
)
}
test "NIST FIPS 180-2: 448-bit two-block message" {
const d = sha256("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq")
try expectDigestHex(
"248D6A61D20638B8E5C026930C3E6039A33CE45964FF2167F6ECEDD419DB06C1",
d[0..]
)
}
test "NIST FIPS 180-2: 896-bit message" {
const d = sha256("abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmno" ++
"ijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu")
try expectDigestHex(
"CF5B16A778AF8380036CE59E7B0492370B249B11E8F07A51AFAC45037AFEE9D1",
d[0..]
)
}
test "NIST FIPS 180-2: one million 'a' (streamed)" {
// Streamed through update() in odd-sized chunks — there is no 1 MB
// buffer anywhere, which is the point of the incremental hasher.
var h = Sha256.init()
const chunk = [_]u8{'a'} ** 1000
var fed usize = 0
while (fed < 1_000_000) {
h.update(chunk[0..])
fed += chunk.len
}
const d = h.final()
try expectDigestHex(
"CDC76E5C9914FB9281A1C7E284D73E67F1809A48A497200E046D39CCC7112CD0",
d[0..]
)
}
test "streaming equivalence: byte-by-byte == chunked == one-shot" {
// Exercises every block-boundary path in update(): a message long
// enough to span multiple blocks, fed three different ways.
var msg [257]u8 = undefined
for *b, i in &msg {
b.* = #truncate(i *% 31 +% 7)
}
const oneshot = sha256(msg[0..])
var by_byte = Sha256.init()
for b in msg {
by_byte.update(&[_]u8{b})
}
const d_byte = by_byte.final()
var chunked = Sha256.init()
chunked.update(msg[0..63])
chunked.update(msg[63..64]) // exactly closes block 1
chunked.update(msg[64..130]) // spans a boundary
chunked.update(msg[130..130]) // empty update
chunked.update(msg[130..])
const d_chunk = chunked.final()
try testing.expectEqualSlices(u8, oneshot[0..], d_byte[0..])
try testing.expectEqualSlices(u8, oneshot[0..], d_chunk[0..])
}
// RFC 4231 groups four cases under one heading; Flash has no bare scope
// block, so each case is its own test (same vectors, isolated bindings).
test "RFC 4231 case 1: 20-byte 0x0B key" {
const key = [_]u8{0x0B} ** 20
const d = hmacSha256(key[0..], "Hi There")
try expectDigestHex(
"B0344C61D8DB38535CA8AFCEAF0BF12B881DC200C9833DA726E9376C2E32CFF7",
d[0..]
)
}
test "RFC 4231 case 2: short ASCII key" {
const d = hmacSha256("Jefe", "what do ya want for nothing?")
try expectDigestHex(
"5BDCC146BF60754E6A042426089575C75A003F089D2739839DEC58B964EC3843",
d[0..]
)
}
test "RFC 4231 case 3: 20-byte 0xAA key, 50-byte 0xDD message" {
const key = [_]u8{0xAA} ** 20
const msg = [_]u8{0xDD} ** 50
const d = hmacSha256(key[0..], msg[0..])
try expectDigestHex(
"773EA91E36800E46854DB8EBD09181A72959098B3EF8C122D9635514CED565FE",
d[0..]
)
}
test "RFC 4231 case 4: 25-byte counting key, 50-byte 0xCD message" {
const key = [_]u8{
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A,
0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14,
0x15, 0x16, 0x17, 0x18, 0x19
}
const msg = [_]u8{0xCD} ** 50
const d = hmacSha256(key[0..], msg[0..])
try expectDigestHex(
"82558A389A443C0EA4CC819899F2083A85F0FAA3E578F8077A2E3FF46729665B",
d[0..]
)
}
// Cases 6-7 share a 131-byte key — longer than the 64-byte block, so
// init() must hash it first.
test "RFC 4231 case 6: 131-byte oversize key, short message" {
const key = [_]u8{0xAA} ** 131
const d = hmacSha256(key[0..], "Test Using Larger Than Block-Size Key - Hash Key First")
try expectDigestHex(
"60E431591EE0B67F0D8A26AACBF5B77F8E0BC6213728C5140546040F0EE37F54",
d[0..]
)
}
test "RFC 4231 case 7: 131-byte oversize key, oversize message" {
const key = [_]u8{0xAA} ** 131
const d = hmacSha256(key[0..], "This is a test using a larger than block-size key and a " ++
"larger than block-size data. The key needs to be hashed " ++
"before being used by the HMAC algorithm.")
try expectDigestHex(
"9B09FFA71B942FCB27635FBCD5B0E944BFDC63644F0713938A7F51535C3A35E2",
d[0..]
)
}
test "PBKDF2-HMAC-SHA256: published vectors (c=1, c=2, c=4096)" {
// The standard PBKDF2-HMAC-SHA256 vector set (the RFC 6070 cases
// re-keyed to SHA-256; cross-published in multiple library test
// suites). The differential test below independently checks the
// implementation against std.crypto, so a transcription error here
// and an implementation error cannot mask each other.
var dk [32]u8 = undefined
pbkdf2HmacSha256("password", "salt", 1, dk[0..])
try expectDigestHex(
"120FB6CFFCF8B32C43E7225256C4F837A86548C92CCC35480805987CB70BE17B",
dk[0..]
)
pbkdf2HmacSha256("password", "salt", 2, dk[0..])
try expectDigestHex(
"AE4D0C95AF6B46D32D0ADFF928F06DD02A303F8EF3C251DFD6E2D85A95474C43",
dk[0..]
)
pbkdf2HmacSha256("password", "salt", 4096, dk[0..])
try expectDigestHex(
"C5E478D59288C841AA530DB6845C4C8D962893A001CE4E11A4963873AA98134A",
dk[0..]
)
}
test "PBKDF2-HMAC-SHA256: multi-block output (dkLen=40)" {
// dkLen=40 forces a second T-block (T_1 full + 8 bytes of T_2).
var dk [40]u8 = undefined
pbkdf2HmacSha256(
"passwordPASSWORDpassword",
"saltSALTsaltSALTsaltSALTsaltSALTsalt",
4096,
dk[0..]
)
try expectDigestHex(
"348C89DBCBD32B2F32D814B8116E84CF2B17347EBC1800181C4E2A1FB8DD53E1C635518C7DAC47E9",
dk[0..]
)
}
test "PBKDF2-HMAC-SHA256: truncated output with embedded NULs (dkLen=16)" {
// dkLen=16 truncates T_1; password and salt carry embedded NULs.
var dk [16]u8 = undefined
pbkdf2HmacSha256("pass\x00word", "sa\x00lt", 4096, dk[0..])
try expectDigestHex(
"89B69D0516F829893C696226650A8687",
dk[0..]
)
}
test "PBKDF2-HMAC-SHA256: RFC 7914 reference vector (dkLen=64)" {
var dk [64]u8 = undefined
pbkdf2HmacSha256("passwd", "salt", 1, dk[0..])
try expectDigestHex(
"55AC046E56E3089FEC1691C22544B605F94185216DDE0465E68B9D57C20DACBC" ++
"49CA9CCCF179B645991664B39D77EF317C71B845B1E30BD509112041D3A19783",
dk[0..]
)
}
test "differential: sha256 matches std.crypto for lengths 0..257" {
// Patterned (deterministic) messages of every length crossing the
// one-block and two-block boundaries. Catches any divergence the
// fixed vectors might miss, with std.crypto as the reference.
var msg [257]u8 = undefined
for *b, i in &msg {
b.* = #truncate(i *% 131 +% 89)
}
var len usize = 0
while (len <= msg.len) {
const ours = sha256(msg[0..len])
var theirs [32]u8 = undefined
std.crypto.hash.sha2.Sha256.hash(msg[0..len], &theirs, .{})
try testing.expectEqualSlices(u8, theirs[0..], ours[0..])
len += 1
}
}
test "differential: HMAC matches std.crypto across key/msg sizes" {
// Key lengths sweep across the block boundary (incl. 0, 64, 65);
// message lengths sweep across block boundaries.
var buf [192]u8 = undefined
for *b, i in &buf {
b.* = #truncate(i *% 37 +% 11)
}
const key_lens = [_]usize{ 0, 1, 31, 32, 63, 64, 65, 128, 192 }
const msg_lens = [_]usize{ 0, 1, 55, 56, 63, 64, 65, 127, 128, 192 }
for kl in key_lens {
for ml in msg_lens {
const ours = hmacSha256(buf[0..kl], buf[0..ml])
var theirs [32]u8 = undefined
std.crypto.auth.hmac.sha2.HmacSha256.create(&theirs, buf[0..ml], buf[0..kl])
try testing.expectEqualSlices(u8, theirs[0..], ours[0..])
}
}
}
const Pbkdf2Case = struct {
pw []u8,
salt []u8,
c u32,
len usize
}
test "differential: PBKDF2 matches std.crypto" {
// Odd dkLen (not a digest multiple), several iteration counts.
const cases = [_]Pbkdf2Case{
.{ .pw = "password", .salt = "salt", .c = 1, .len = 20 },
.{ .pw = "password", .salt = "salt", .c = 100, .len = 33 },
.{ .pw = "", .salt = "salt", .c = 7, .len = 32 },
.{ .pw = "password", .salt = "", .c = 7, .len = 32 },
.{ .pw = "a-fairly-long-password-beyond-one-sha-block-aaaaaaaaaaaaaaaaaaaaaaaaaa", .salt = "pepper", .c = 13, .len = 48 }
}
for case in cases {
var ours [64]u8 = undefined
var theirs [64]u8 = undefined
pbkdf2HmacSha256(case.pw, case.salt, case.c, ours[0..case.len])
try std.crypto.pwhash.pbkdf2(theirs[0..case.len], case.pw, case.salt, case.c, std.crypto.auth.hmac.sha2.HmacSha256)
try testing.expectEqualSlices(u8, theirs[0..case.len], ours[0..case.len])
}
}