Skip to content
Open
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
76 changes: 76 additions & 0 deletions vlib/crypto/blake2b/blake2b.v
Original file line number Diff line number Diff line change
Expand Up @@ -375,3 +375,79 @@ pub fn pmac160(data []u8, key []u8) []u8 {
d.write(data) or { panic(err) }
return d.checksum_internal() or { panic(err) }
}

// sum returns the Blake2b checksum of the data with the specified output length (1-64 bytes).
pub fn sum(data []u8, outlen int) []u8 {
if outlen < 1 || outlen > size512 {
panic('blake2b: invalid output length ${outlen}, must be between 1 and ${size512}')
}
mut d := new_digest(u8(outlen), []u8{}) or { panic(err) }
d.write(data) or { panic(err) }
return d.checksum_internal() or { panic(err) }
}

// blake2b_long produces a hash of arbitrary length using the Blake2b extension mechanism.
// This is used by Argon2 for variable-length output greater than 64 bytes.
// For outputs <= 64 bytes, it's equivalent to sum().
// For outputs > 64 bytes, it uses iterative hashing as specified in the Argon2 RFC.
pub fn blake2b_long(data []u8, outlen int) []u8 {
if outlen < 1 {
return []u8{}
}

// Prepend output length as little-endian u32
outlen_bytes := [
u8(outlen & 0xFF),
u8((outlen >> 8) & 0xFF),
u8((outlen >> 16) & 0xFF),
u8((outlen >> 24) & 0xFF),
]

mut full_input := outlen_bytes.clone()
full_input << data

if outlen <= size512 {
// Simple case: hash outlen_bytes || input
return sum(full_input, outlen)
}

// For longer outputs, implement the extension mechanism
mut out := []u8{len: outlen}
mut toproduce := outlen

// First, hash to get initial output
out_buffer := sum512(full_input)

// Copy first half to output
copy_len := size512 / 2
for i in 0 .. copy_len {
out[i] = out_buffer[i]
}

mut offset := copy_len
toproduce -= copy_len
mut in_buffer := out_buffer.clone()

// Generate additional output by iterative hashing
for toproduce > size512 {
out_buffer2 := sum512(in_buffer)

// Copy half of the output
for i in 0 .. size512 / 2 {
out[offset + i] = out_buffer2[i]
}
offset += size512 / 2
toproduce -= size512 / 2
in_buffer = out_buffer2.clone()
}

// Final iteration
if toproduce > 0 {
final_output := sum(in_buffer, toproduce)
for i in 0 .. toproduce {
out[offset + i] = final_output[i]
}
}

return out
}
135 changes: 135 additions & 0 deletions vlib/crypto/blake2b/blake2b_test.v
Original file line number Diff line number Diff line change
Expand Up @@ -2600,3 +2600,138 @@ fn test_all_vectors() {
assert hash == output, 'input: ${v.input} key: ${v.key}'
}
}

// Tests for sum() - variable length output (1-64 bytes)
fn test_sum_output_lengths() {
data := 'Hello, World!'.bytes()

// Test various output lengths
for outlen in [1, 16, 20, 32, 48, 64] {
result := blake2b.sum(data, outlen)
assert result.len == outlen, 'sum() should return ${outlen} bytes, got ${result.len}'
}
}

fn test_sum_consistency_with_fixed_functions() {
data := 'The quick brown fox jumps over the lazy dog'.bytes()

// sum(64) should match sum512()
assert blake2b.sum(data, 64) == blake2b.sum512(data), 'sum(64) should equal sum512()'

// sum(48) should match sum384()
assert blake2b.sum(data, 48) == blake2b.sum384(data), 'sum(48) should equal sum384()'

// sum(32) should match sum256()
assert blake2b.sum(data, 32) == blake2b.sum256(data), 'sum(32) should equal sum256()'

// sum(20) should match sum160()
assert blake2b.sum(data, 20) == blake2b.sum160(data), 'sum(20) should equal sum160()'
}

fn test_sum_empty_input() {
empty := []u8{}

// Empty input should still produce valid hashes
result := blake2b.sum(empty, 32)
assert result.len == 32, 'sum() with empty input should return 32 bytes'

// sum(32) produces a different hash than sum512 truncated, because
// the output length is part of the Blake2b parameter block.
// Verify it matches sum256() which also uses 32-byte output length.
assert result == blake2b.sum256(empty), 'sum(empty, 32) should match sum256(empty)'
}

fn test_sum_deterministic() {
data := 'deterministic test'.bytes()

// Same input should always produce same output
result1 := blake2b.sum(data, 32)
result2 := blake2b.sum(data, 32)
assert result1 == result2, 'sum() should be deterministic'
}

// Tests for blake2b_long() - arbitrary length output
fn test_blake2b_long_short_output() {
data := 'test data'.bytes()

// For outputs <= 64 bytes, blake2b_long prepends length
for outlen in [1, 16, 32, 48, 64] {
result := blake2b.blake2b_long(data, outlen)
assert result.len == outlen, 'blake2b_long(${outlen}) should return ${outlen} bytes, got ${result.len}'
}
}

fn test_blake2b_long_extended_output() {
data := 'extended output test'.bytes()

// Test outputs > 64 bytes (uses iterative hashing)
for outlen in [65, 100, 128, 256, 512, 1024] {
result := blake2b.blake2b_long(data, outlen)
assert result.len == outlen, 'blake2b_long(${outlen}) should return ${outlen} bytes, got ${result.len}'
}
}

fn test_blake2b_long_deterministic() {
data := 'deterministic long test'.bytes()

// Same input should always produce same output
result1 := blake2b.blake2b_long(data, 256)
result2 := blake2b.blake2b_long(data, 256)
assert result1 == result2, 'blake2b_long() should be deterministic'
}

fn test_blake2b_long_different_lengths_differ() {
data := 'length test'.bytes()

// Different output lengths should produce different results
result64 := blake2b.blake2b_long(data, 64)
result128 := blake2b.blake2b_long(data, 128)

// First 64 bytes should NOT be the same (due to length prefix in input)
assert result64 != result128[..64], 'blake2b_long with different lengths should differ'
}

fn test_blake2b_long_known_vectors() {
// Test vector: empty input with various output lengths
// These are regression tests to ensure the implementation doesn't change
empty := []u8{}

// blake2b_long prepends the output length as LE u32, so:
// blake2b_long(empty, 32) = blake2b([0x20, 0x00, 0x00, 0x00], 32)
result32 := blake2b.blake2b_long(empty, 32)
assert result32.len == 32

// blake2b_long(empty, 64) = blake2b([0x40, 0x00, 0x00, 0x00], 64)
result64 := blake2b.blake2b_long(empty, 64)
assert result64.len == 64

// Verify the length prefix behavior: different output lengths = different hashes
assert result32 != result64[..32], 'length prefix should affect hash output'
}

fn test_blake2b_long_argon2_use_case() {
// Argon2 uses blake2b_long to generate 1024-byte block hashes
// This tests the specific use case that motivated adding this function
block_data := []u8{len: 72, init: u8(index % 256)}

result := blake2b.blake2b_long(block_data, 1024)
assert result.len == 1024, 'blake2b_long should support 1024-byte output for Argon2'

// Verify it's not all zeros or repetitive
mut has_nonzero := false
for b in result {
if b != 0 {
has_nonzero = true
break
}
}
assert has_nonzero, 'blake2b_long output should not be all zeros'
}

fn test_blake2b_long_empty_output() {
data := 'test'.bytes()

// outlen < 1 should return empty
result := blake2b.blake2b_long(data, 0)
assert result.len == 0, 'blake2b_long(0) should return empty slice'
}
Loading