~sircmpwn/himitsu-devel

This thread contains a patchset. You're looking at the original emails, but you may wish to use the patch review UI. Review patch
5 2

[PATCH himitsu v2 1/5] move encryption into separate fn

Details
Message ID
<20230423080713.1986-1-apreiml@strohwolke.at>
DKIM signature
missing
Download raw message
Patch: +62 -54
---

v2: Minor update on error handling in secstore

 secstore/secstore.ha  | 88 ++++++++++++++++++++++++++-----------------
 secstore/serialize.ha | 28 ++++----------
 2 files changed, 62 insertions(+), 54 deletions(-)

diff --git a/secstore/secstore.ha b/secstore/secstore.ha
index 2c82d57..d4ca53a 100644
--- a/secstore/secstore.ha
+++ b/secstore/secstore.ha
@@ -69,6 +69,52 @@ export fn create(passphrase: []u8) (secstore | error) = {
	};
};

// Encrypts msg and writes it to sink
fn secbox(
	sink: io::handle,
	key: (keystore::key | []u8),
	msg: []u8
) (void | io::error) = {
	let keybuf: [32]u8 = [0...];
	defer bytes::zero(keybuf);
	match (key) {
	case let key: keystore::key =>
		keystore::read(key, keybuf);
	case let key: []u8 =>
		keybuf[..] = key[..];
	};

	let nonce: crypto::nonce = [0...];
	random::buffer(nonce);

	const box = crypto::encrypt(&keybuf, &nonce, msg);

	io::write(sink, box.0)?;
	io::write(sink, box.1)?;
	io::write(sink, box.2)?;
};

// Reads the ciphertext created with [[secbox]] from 'src' and returns the
// plaintext. The user must free the plaintext after use.
fn secunbox(src: io::handle, key: (keystore::key | []u8)) ([]u8 | io::error) = {
	let keybuf: [32]u8 = [0...];
	defer bytes::zero(keybuf);
	match (key) {
	case let key: keystore::key =>
		keystore::read(key, keybuf);
	case let key: []u8 =>
		keybuf[..] = key[..];
	};

	let mac: crypto::mac = [0...];
	let nonce: crypto::nonce = [0...];
	io::readall(src, mac)?;
	io::readall(src, nonce)?;
	let ciphertext = io::drain(src)?;
	let box: crypto::box = (mac, nonce, ciphertext);
	return crypto::decrypt(&keybuf, &box)?;
};

// Opens the secstore. The caller should call [[unlock]] to decrypt it, and
// [[close]] when they're done with it.
export fn open() (secstore | error) = {
@@ -242,18 +288,12 @@ fn add_secret(store: *secstore, val: str) uuid::uuid = {
	const file = os::create(path::string(&buf), 0o600)!;
	defer io::close(file)!;

	let key: [32]u8 = [0...];
	defer bytes::zero(key);
	keystore::read(store.key as keystore::key, key);

	let nonce: crypto::nonce = [0...];
	random::buffer(nonce);
	let ciphertext = strings::toutf8(strings::dup(val));
	defer free(ciphertext);
	const box = crypto::encrypt(&key, &nonce, ciphertext);
	io::write(file, box.0)!;
	io::write(file, box.1)!;
	io::write(file, box.2)!;
	let plain = strings::toutf8(strings::dup(val));
	defer {
		bytes::zero(plain);
		free(plain);
	};
	secbox(file, store.key as keystore::key, plain)!;
	return id;
};

@@ -284,18 +324,8 @@ fn add_index(store: *secstore, entry: *entry) void = {

	const buf = bufio::buffer(&buf);

	let key: [32]u8 = [0...];
	defer bytes::zero(key);
	keystore::read(store.key as keystore::key, key);

	let nonce: crypto::nonce = [0...];
	random::buffer(nonce);

	const box = crypto::encrypt(&key, &nonce, buf);
	const enc = base64::newencoder(&base64::std_encoding, store.index);
	io::write(&enc, box.0)!;
	io::write(&enc, box.1)!;
	io::write(&enc, box.2)!;
	secbox(&enc, store.key as keystore::key, buf)!;
	io::close(&enc)!;
	fmt::fprintln(store.index)!;
};
@@ -320,18 +350,8 @@ fn load_index(store: *secstore) (void | io::error | errors::invalid) = {
		const buf = bufio::fixed(line, io::mode::READ);
		const dec = base64::newdecoder(&base64::std_encoding, &buf);

		let mac: crypto::mac = [0...];
		let nonce: crypto::nonce = [0...];
		io::readall(&dec, mac)?;
		io::readall(&dec, nonce)?;
		let ciphertext = io::drain(&dec)?;
		defer {
			bytes::zero(ciphertext);
			free(ciphertext);
		};
		const plaintext = secunbox(&dec, key)?;

		let box: crypto::box = (mac, nonce, ciphertext);
		const plaintext = crypto::decrypt(&key, &box)?;
		const buf = bufio::fixed(plaintext, io::mode::READ);
		const q = query::parse(&buf)!;
		defer query::finish(&q);
diff --git a/secstore/serialize.ha b/secstore/serialize.ha
index 1e1a3c8..7523189 100644
--- a/secstore/serialize.ha
+++ b/secstore/serialize.ha
@@ -67,28 +67,16 @@ fn write_private(
	const file = os::open(path::string(&buf))?;
	defer io::close(file)!;

	let mac: crypto::mac = [0...];
	let nonce: crypto::nonce = [0...];
	io::readall(file, mac)?;
	io::readall(file, nonce)?;
	let ciphertext = io::drain(file)?;
	defer {
		bytes::zero(ciphertext);
		free(ciphertext);
	};

	let key: [32]u8 = [0...];
	defer bytes::zero(key);
	keystore::read(store.key as keystore::key, key);

	let box: crypto::box = (mac, nonce, ciphertext);
	const plaintext = match (crypto::decrypt(&key, &box)) {
	case errors::invalid =>
	let plaintext = match (secunbox(file, store.key as keystore::key)) {
	case let b: []u8 =>
		yield b;
	case =>
		return badstore;
	case let buf: []u8 =>
		yield buf;
	};
	defer bytes::zero(plaintext);
	defer {
		bytes::zero(plaintext);
		free(plaintext);
	};

	io::writeall(sink, plaintext)?;
};
-- 
2.40.0

[PATCH himitsu v2 2/5] secstore: encrypted master key

Details
Message ID
<20230423080713.1986-2-apreiml@strohwolke.at>
In-Reply-To
<20230423080713.1986-1-apreiml@strohwolke.at> (view parent)
DKIM signature
missing
Download raw message
Patch: +76 -13
---
 secstore/secstore.ha | 89 +++++++++++++++++++++++++++++++++++++-------
 1 file changed, 76 insertions(+), 13 deletions(-)

diff --git a/secstore/secstore.ha b/secstore/secstore.ha
index d4ca53a..d01eb0f 100644
--- a/secstore/secstore.ha
+++ b/secstore/secstore.ha
@@ -21,6 +21,14 @@ use uuid;
def KEY_MEM: u32 = 100000;
def KEY_PASSES: u32 = 0;

type keyversion = enum u8 {
	// Buggy argon2 key
	V0 = 0,

	// argon2 key encrypted master key
	V1 = 1,
};

// Initializes a new secstore using the provided passphrase. The caller should
// call [[close]] when they're done with it.
export fn create(passphrase: []u8) (secstore | error) = {
@@ -29,7 +37,6 @@ export fn create(passphrase: []u8) (secstore | error) = {

	let salt: [16]u8 = [0...];
	random::buffer(salt);
	// TODO: Switch to argon2id by default
	const config = argon2::config {
		mem = KEY_MEM,
		parallel = 1,
@@ -37,10 +44,9 @@ export fn create(passphrase: []u8) (secstore | error) = {
		version = argon2::VERSION,
		...
	};
	argon2::argon2i(key, passphrase, salt, &config)!;
	argon2::argon2id(key, passphrase, salt, &config)!;

	const verify = key[32..];
	const key = keystore::newkey(key[..32], "secstore")!;
	const dir = strings::dup(dirs::data("himitsu"));

	let buf = path::init();
@@ -51,17 +57,29 @@ export fn create(passphrase: []u8) (secstore | error) = {
	const keyfile = &bufio::buffered(keyfile, [], wbuf);
	defer io::close(keyfile)!;

	fmt::fprintf(keyfile, "argon2i:{}:{}:", KEY_MEM, KEY_PASSES)?;
	fmt::fprintf(keyfile, "argon2id:{}:{}:", KEY_MEM, KEY_PASSES)?;
	base64::encode(keyfile, &base64::std_encoding, salt)?;
	fmt::fprintf(keyfile, ":")?;
	base64::encode(keyfile, &base64::std_encoding, verify)?;
	fmt::fprintln(keyfile)?;
	fmt::fprintf(keyfile, ":{}:", keyversion::V1: u8)?;

	let masterkeybuf: [32]u8 = [0...];
	random::buffer(masterkeybuf);
	defer bytes::zero(masterkeybuf);
	let masterkey = keystore::newkey(masterkeybuf, "secstore")!;

	let mkkey = keystore::newkey(key[..32], "masterkeykey")!;
	defer keystore::destroy(mkkey);

	let enc = base64::newencoder(&base64::std_encoding, keyfile);
	secbox(&enc, mkkey, masterkeybuf)!;
	io::close(&enc)!;

	path::set(&buf, dir, "index")!;
	const index = os::create(path::string(&buf), 0o600)?;

	return secstore {
		key = key,
		key = masterkey,
		state = state::UNLOCKED,
		dir = dir,
		index = index,
@@ -151,13 +169,10 @@ export fn unlock(store: *secstore, passphrase: []u8) (void | error) = {
	defer free(keydata);
	const items = strings::split(keydata, ":");
	defer free(items);
	if (len(items) != 5) {
	if (len(items) != 5 && len(items) != 7) {
		return badstore;
	};

	if (items[0] != "argon2i") {
		return badstore;
	};
	const mem = match (strconv::stou32(items[1])) {
	case let u: u32 =>
		yield u;
@@ -191,10 +206,23 @@ export fn unlock(store: *secstore, passphrase: []u8) (void | error) = {
		return badstore;
	};

	let kv = if (len(items) == 5) {
		yield keyversion::V0;
	} else {
		yield match (strconv::stou8(items[5])) {
		case let v: u8 =>
			if (v > keyversion::V1) {
				return badstore;
			};
			yield v: keyversion;
		case =>
			return badstore;
		};
	};

	let key: [32 + 16]u8 = [0...];
	defer bytes::zero(key);

	// TODO: Switch to argon2id by default
	const config = argon2::config {
		mem = mem,
		parallel = 1,
@@ -202,13 +230,28 @@ export fn unlock(store: *secstore, passphrase: []u8) (void | error) = {
		version = argon2::VERSION,
		...
	};
	argon2::argon2i(key, passphrase, salt, &config)!;
	switch (items[0]) {
	case "argon2i" =>
		argon2::argon2i(key, passphrase, salt, &config)!;
	case "argon2id" =>
		argon2::argon2id(key, passphrase, salt, &config)!;
	case =>
		return badstore;
	};

	if (!crypto::compare(key[32..], verify)) {
		return badpass;
	};

	store.key = keystore::newkey(key[..32], "secstore")!;
	store.key = switch (kv) {
	case keyversion::V0 =>
		yield loadv0key(key[..32]);
	case keyversion::V1 =>
		yield loadv1key(key[..32], items[6])?;
	case =>
		abort();
	};

	store.state = state::UNLOCKED;
	match (load_index(store)) {
	case void => yield;
@@ -219,6 +262,26 @@ export fn unlock(store: *secstore, passphrase: []u8) (void | error) = {
	};
};

fn loadv0key(key: []u8) keystore::key =
	keystore::newkey(key[..32], "secstore")!;

fn loadv1key(key: []u8, enckey: str) (keystore::key | error) = {
	const buf = bufio::fixed(strings::toutf8(enckey), io::mode::READ);
	const dec = base64::newdecoder(&base64::std_encoding, &buf);
	let masterkey = match (secunbox(&dec, key)) {
	case let b: []u8 =>
		yield b;
	case =>
		return badstore;
	};
	defer {
		bytes::zero(masterkey);
		free(masterkey);
	};

	return keystore::newkey(masterkey, "secstore")!;
};

// Locks the key store, unloading the decryption keys.
export fn lock(store: *secstore) void = {
	match (store.key) {
-- 
2.40.0

[PATCH himitsu v2 3/5] secstore: vendor buggy argon2 implementation

Details
Message ID
<20230423080713.1986-3-apreiml@strohwolke.at>
In-Reply-To
<20230423080713.1986-1-apreiml@strohwolke.at> (view parent)
DKIM signature
missing
Download raw message
Patch: +675 -1
---
 crypto/argon2bug/+test.ha  | 131 ++++++++++
 crypto/argon2bug/README    |  30 +++
 crypto/argon2bug/argon2.ha | 508 +++++++++++++++++++++++++++++++++++++
 secstore/secstore.ha       |   7 +-
 4 files changed, 675 insertions(+), 1 deletion(-)
 create mode 100644 crypto/argon2bug/+test.ha
 create mode 100644 crypto/argon2bug/README
 create mode 100644 crypto/argon2bug/argon2.ha

diff --git a/crypto/argon2bug/+test.ha b/crypto/argon2bug/+test.ha
new file mode 100644
index 0000000..e9038e4
--- /dev/null
+++ b/crypto/argon2bug/+test.ha
@@ -0,0 +1,131 @@
// License: MPL-2.0
// (c) 2021-2022 Armin Preiml <apreiml@strohwolke.at>
// (c) 2022 Drew DeVault <sir@cmpwn.com>
use bytes;

@test fn mode_d_one_pass() void = {
	let pass: [32]u8 = [1...];
	let salt: [16]u8 = [2...];
	let secret: [8]u8 = [3...];
	let data: [12]u8 = [4...];
	let result: [32]u8 = [0...];

	let expected: [_]u8 = [
		0xfa, 0x17, 0x75, 0xca, 0x80, 0x90, 0x64, 0x66, 0x18, 0xbe,
		0x70, 0xeb, 0x0f, 0xc9, 0xde, 0x43, 0x67, 0x58, 0xed, 0x0c,
		0xa5, 0x36, 0x83, 0x1a, 0xe9, 0xe1, 0x03, 0x48, 0x93, 0x81,
		0xc1, 0x79,
	];

	let cfg = config {
		secret = secret,
		data = data,
		passes = 1,
		parallel = 4,
		version = 0x13,
		mem = 32,
		...
	};

	argon2d(result[..], pass, salt, &cfg)!;

	assert(bytes::equal(result, expected));
};

@test fn rfc_d_test_vector() void = {
	let pass: [32]u8 = [1...];
	let salt: [16]u8 = [2...];
	let secret: [8]u8 = [3...];
	let data: [12]u8 = [4...];
	let result: [32]u8 = [0...];

	let mem: []u64 = alloc([0...], 32z * BLOCKSIZE);
	defer free(mem);

	let expected: [_]u8 = [
		0x51, 0x2b, 0x39, 0x1b, 0x6f, 0x11, 0x62, 0x97, 0x53, 0x71,
		0xd3, 0x09, 0x19, 0x73, 0x42, 0x94, 0xf8, 0x68, 0xe3, 0xbe,
		0x39, 0x84, 0xf3, 0xc1, 0xa1, 0x3a, 0x4d, 0xb9, 0xfa, 0xbe,
		0x4a, 0xcb,
	];

	let cfg = config {
		secret = secret,
		data = data,
		passes = 3,
		parallel = 4,
		version = 0x13,
		mem = mem[..],
		...
	};

	argon2d(result[..], pass, salt, &cfg)!;

	assert(bytes::equal(result, expected));
};


@test fn rfc_i_test_vector() void = {
	let pass: [32]u8 = [1...];
	let salt: [16]u8 = [2...];
	let secret: [8]u8 = [3...];
	let data: [12]u8 = [4...];
	let result: [32]u8 = [0...];

	let mem: []u64 = alloc([0...], 32z * BLOCKSIZE);
	defer free(mem);

	let expected: [_]u8 = [
		0xc8, 0x14, 0xd9, 0xd1, 0xdc, 0x7f, 0x37, 0xaa, 0x13, 0xf0,
		0xd7, 0x7f, 0x24, 0x94, 0xbd, 0xa1, 0xc8, 0xde, 0x6b, 0x01,
		0x6d, 0xd3, 0x88, 0xd2, 0x99, 0x52, 0xa4, 0xc4, 0x67, 0x2b,
		0x6c, 0xe8,
	];

	let cfg = config {
		secret = secret,
		data = data,
		passes = 3,
		parallel = 4,
		version = 0x13,
		mem = mem[..],
		...
	};

	argon2i(result[..], pass, salt, &cfg)!;

	assert(bytes::equal(result, expected));
};

@test fn rfc_id_test_vector() void = {
	let pass: [32]u8 = [1...];
	let salt: [16]u8 = [2...];
	let secret: [8]u8 = [3...];
	let data: [12]u8 = [4...];
	let result: [32]u8 = [0...];

	let mem: []u64 = alloc([0...], 32z * BLOCKSIZE);
	defer free(mem);

	let expected: [_]u8 = [
		0x0d, 0x64, 0x0d, 0xf5, 0x8d, 0x78, 0x76, 0x6c, 0x08, 0xc0,
		0x37, 0xa3, 0x4a, 0x8b, 0x53, 0xc9, 0xd0, 0x1e, 0xf0, 0x45,
		0x2d, 0x75, 0xb6, 0x5e, 0xb5, 0x25, 0x20, 0xe9, 0x6b, 0x01,
		0xe6, 0x59,
	];

	let cfg = config {
		secret = secret,
		data = data,
		passes = 3,
		parallel = 4,
		version = 0x13,
		mem = mem[..],
		...
	};

	argon2id(result[..], pass, salt, &cfg)!;

	assert(bytes::equal(result, expected));
};

diff --git a/crypto/argon2bug/README b/crypto/argon2bug/README
new file mode 100644
index 0000000..b06abdf
--- /dev/null
+++ b/crypto/argon2bug/README
@@ -0,0 +1,30 @@
This module provides an implementation of the argon2 key derivation function as
described by RFC 9106. This is the recommended algorithm for password hashing in
Hare programs, and for deriving keys for use with other cryptographic
algorithms. Some thought must be given to the appropriate configuration for your
use case. Some general advice is provided here; if in doubt, consult the RFC.

The argon2 parameters are configured via the [[config]] structure. To determine
the appropriate configuration parameters for a particular use-case, consult
section 4 of the RFC. Otherwise, sane defaults for common scenarios are provided
via [[default_config]] and [[low_mem_config]]; consult the docs of each
configuration for details.

Once a suitable configuration has been selected, the user must provide a salt.
This salt should be stored alongside the hash, should be unique for each
password, and should be random: see [[crypto::random]]. The salt and hash
lengths are configurable, the recommended defaults are 16 and 32 bytes
respectively.

Equipped with the necessary parameters, the user may call the appropriate argon2
variant via [[argon2d]], [[argon2i]], or [[argon2id]]. If unsure which to use,
choose [[argon2id]]. The RFC is the authoratative source on the appropriate
argon2 variant and configuration parameters for your use-case.

This is a low-level module which implements cryptographic primitives. Direct use
of cryptographic primitives is not recommended for non-experts, as incorrect use
of these primitives can easily lead to the introduction of security
vulnerabilities. Non-experts are advised to use the high-level operations
available in the top-level [[crypto]] module.

Be advised that Hare's cryptography implementations have not been audited.
diff --git a/crypto/argon2bug/argon2.ha b/crypto/argon2bug/argon2.ha
new file mode 100644
index 0000000..558e161
--- /dev/null
+++ b/crypto/argon2bug/argon2.ha
@@ -0,0 +1,508 @@
// License: MPL-2.0
// (c) 2022 Alexey Yerin <yyp@disroot.org>
// (c) 2021-2022 Armin Preiml <apreiml@strohwolke.at>
// (c) 2021-2022 Drew DeVault <sir@cmpwn.com>
use bufio;
use bytes;
use crypto::blake2b;
use crypto::math;
use endian;
use errors::{nomem};
use hash;
use io;
use types;

// Latest version of argon2 supported by this implementation (1.3).
export def VERSION: u8 = 0x13;

// Number of u64 elements of one block.
export def BLOCKSIZE: u32 = 128;

def SLICES: size = 4;

type block64 = [BLOCKSIZE]u64;

const zeroblock: block64 = [0...];

type mode = enum {
	D = 0,
	I = 1,
	ID = 2,
};

// This type provides configuration options for the argon2 algorithm. Most users
// will find [[default_config]] or [[low_mem_config]] suitable for their needs
// without providing a custom configuration. If writing a custom configuration,
// consult the RFC for advice on selecting suitable values for your use-case.
//
// 'parallel' specifies the number of parallel processes. 'pass' configures the
// number of iterations. Both values must be at least one. Note: the Hare
// implementation of argon2 does not process hashes in parallel, though it will
// still compute the correct hash if this value is greater than one.
//
// 'version' specifies the version of the argon2 function. The implementation
// currently only supports version 1.3. Use [[VERSION]] here.
//
// 'secret' and 'data' are optional byte arrays that are applied to the initial
// state. Consult the RFC for details.
//
// The 'mem' parameter is used to configure working memory used during the
// computation. The argon2 algorithm requires a large amount of memory to
// compute hashes. If 'mem' set to a u32, it is interpreted as the desired
// number of 1024-byte blocks the implementation shall allocate for you. If the
// caller wants to manage the allocation itself, provide a []u8 instead. The
// length of this slice must be at least 8 times the value of 'parallel' in
// blocks, and must be a multiple of [[BLOCKSIZE]]. To have the implementation
// allocate 64 KiB, set 'mem' to 64. To use the same amount of caller-provided
// memory, provide a slice of length 64 * [[BLOCKSIZE]].
export type config = struct {
	mem: (u32 | []u64),
	parallel: u32,
	passes: u32,
	version: u8,
	secret: []u8,
	data: []u8
};

// The default recommended configuration for most use cases. This configuration
// uses 2 GiB of working memory. A 16-byte 'salt' and 32-byte 'dest' parameter
// is recommended in combination with this configuration.
export const default_config: config = config {
	mem = 2 * 1024 * 1024,
	passes = 1,
	parallel = 4,
	version = 0x13,
	...
};

// The default recommended configuration for memory-constrained use cases. This
// configuration uses 64 MiB of working memory. A 16-byte 'salt' and 32-byte
// 'dest' parameter is recommended in combination with this configuration.
export const low_mem_config: config = config {
	mem = 64 * 1024,
	passes = 3,
	parallel = 4,
	version = 0x13,
	...
};

type context = struct {
	mode: mode,
	cols: size,
	rows: size,
	sliceblocks: size,
	mem: []u64,
	pass: u32,
	seedsinit: block64,
	seedblock: block64,
};

// Computes an argon2d hash, writing the digest to 'dest'. A 'salt' length of 16
// bytes is recommended, and 8 bytes is the minimum. A 'dest' length of 32 bytes
// is recommended, and 4 bytes is the minimum.
//
// The argon2d mode uses data-dependent memory access and is suitable for
// applications with no threats of side-channel timing attacks.
export fn argon2d(
	dest: []u8,
	password: []u8,
	salt: []u8,
	cfg: *config,
) (void | nomem) = {
	return argon2(dest, password, salt, cfg, mode::D);
};

// Computes an argon2i hash, writing the digest to 'dest'. A 'salt' length of 16
// bytes is recommended, and 8 bytes is the minimum. A 'dest' length of 32 bytes
// is recommended, and 4 bytes is the minimum.
//
// The argon2i mode uses data-independent memory access and is suitable for
// password hashing and key derivation. It makes more passes over memory to
// protect from trade-off attacks.
export fn argon2i(
	dest: []u8,
	password: []u8,
	salt: []u8,
	cfg: *config,
) (void | nomem) = {
	return argon2(dest, password, salt, cfg, mode::I);
};

// Computes an argon2id hash, writing the digest to 'dest'. A 'salt' length of
// 16 bytes is recommended, and 8 bytes is the minimum. A 'dest' length of 32
// bytes is recommended, and 4 bytes is the minimum.
//
// The argon2id mode works by using argon2i for the first half of the first pass
// and argon2d further on. It provides therefore protection from side-channel
// attacks and brute-force cost savings due to memory trade-offs.
//
// If you are unsure which variant to use, argon2id is recommended.
export fn argon2id(
	dest: []u8,
	password: []u8,
	salt: []u8,
	cfg: *config,
) (void | nomem) = {
	return argon2(dest, password, salt, cfg, mode::ID);
};

fn argon2(
	dest: []u8,
	password: []u8,
	salt: []u8,
	cfg: *config,
	mode: mode,
) (void | nomem) = {
	assert(endian::host == &endian::little, "TODO big endian support");

	assert(len(dest) >= 4 && len(dest) <= types::U32_MAX);
	assert(len(password) <= types::U32_MAX);
	assert(len(salt) >= 8 && len(salt) <= types::U32_MAX);
	assert(cfg.parallel >= 1);
	assert(cfg.passes >= 1);
	assert(len(cfg.secret) <= types::U32_MAX);
	assert(len(cfg.data) <= types::U32_MAX);

	let mem: []u64 = match (cfg.mem) {
	case let mem: []u64 =>
		assert(len(mem) >= 8 * cfg.parallel * BLOCKSIZE
			&& len(mem) % BLOCKSIZE == 0
			&& len(mem) / BLOCKSIZE <= types::U32_MAX);
		yield mem;
	case let memsize: u32 =>
		assert(memsize >= 8 * cfg.parallel
			&& memsize <= types::U32_MAX);

		let membytes = memsize * BLOCKSIZE * 8;
		let mem: []u64 = alloc([0...], membytes);
		yield mem[..membytes / 8];
	};
	// round down memory to nearest multiple of 4 times parallel
	mem = mem[..len(mem) - len(mem) % (4 * cfg.parallel * BLOCKSIZE)];
	const memsize = (len(mem) / BLOCKSIZE): u32;

	let h0: [64]u8 = [0...];
	inithash(&h0, len(dest): u32, password, salt, cfg, mode, memsize);

	const cols = 4 * (memsize / (4 * cfg.parallel));
	let ctx = context {
		rows = cfg.parallel,
		cols = cols,
		sliceblocks = cols / 4,
		pass = 0,
		mem = mem,
		mode = mode,
		seedsinit = [0...],
		seedblock = [0...],
		...
	};

	// hash first and second blocks of each row
	for (let i = 0z; i < ctx.rows; i += 1) {
		let src: [72]u8 = [0...];
		src[..64] = h0[..];

		endian::leputu32(src[64..68], 0);
		endian::leputu32(src[68..], i: u32);
		varhash(blocku8(&ctx, i, 0), src);

		endian::leputu32(src[64..68], 1);
		endian::leputu32(src[68..], i: u32);
		varhash(blocku8(&ctx, i, 1), src);
	};

	// process segments
	for (ctx.pass < cfg.passes; ctx.pass += 1) {
		for (let s = 0z; s < SLICES; s += 1) {
			for (let i = 0z; i < ctx.rows; i += 1) {
				segproc(cfg, &ctx, i, s);
			};
		};
	};

	// final hash
	let b = blocku8(&ctx, 0, ctx.cols - 1);
	for (let i = 1z; i < ctx.rows; i += 1) {
		math::xor(b, b, blocku8(&ctx, i, ctx.cols - 1));
	};

	varhash(dest, b);

	bytes::zero((h0: []u64: *[*]u8)[..len(h0) * size(u64)]);
	bytes::zero((ctx.mem: *[*]u8)[..len(ctx.mem) * size(u64)]);

	if (cfg.mem is u32) {
		// mem was allocated internally
		free(ctx.mem);
	};
};

fn block(ctx: *context, i: size, j: size) []u64 = {
	let index = (ctx.cols * i + j) * BLOCKSIZE;
	return ctx.mem[index..index + BLOCKSIZE];
};

fn blocku8(ctx: *context, i: size, j: size) []u8 = {
	return (block(ctx, i, j): *[*]u8)[..BLOCKSIZE * size(u64)];
};

fn refblock(cfg: *config, ctx: *context, seed: u64, i: size, j: size) []u64 = {
	const j1: u64 = seed & 0xffffffff;
	const segstart = j - (j % ctx.sliceblocks);

	const l: u64 = if (segstart == 0 && ctx.pass == 0) {
		yield 0;
	} else {
		yield (seed >> 32) % cfg.parallel;
	};

	let poolstart: u64 = 0;
	let poolsize: u64 = segstart;
	if (ctx.pass > 0) {
		poolstart = (segstart + ctx.sliceblocks) % ctx.cols;
		poolsize = 3 * ctx.sliceblocks;
	};

	if (l == i: u64) {
		poolsize += (j - segstart);
	};

	if ((j - segstart) == 0 || l == i: u64) {
		poolsize -= 1;
	};

	const x: u64 = (j1 * j1) >> 32;
	const y: u64 = (poolsize * x) >> 32;
	const z: u64 = (poolstart + poolsize - (y+1)) % ctx.cols: u64;

	return block(ctx, l: size, z: size);
};

fn inithash(
	dest: *[64]u8,
	taglen: u32,
	password: []u8,
	salt: []u8,
	cfg: *config,
	mode: mode,
	memsize: u32,
) void = {
	let u32buf: [4]u8 = [0...];
	let h = blake2b::blake2b([], 64);
	defer hash::close(&h);

	hash_leputu32(&h, cfg.parallel);
	hash_leputu32(&h, taglen);
	hash_leputu32(&h, memsize);
	hash_leputu32(&h, cfg.passes);
	hash_leputu32(&h, cfg.version);
	hash_leputu32(&h, mode: u32);
	hash_leputu32(&h, len(password): u32);
	hash::write(&h, password);

	hash_leputu32(&h, len(salt): u32);
	hash::write(&h, salt);

	hash_leputu32(&h, len(cfg.secret): u32);
	hash::write(&h, cfg.secret);

	hash_leputu32(&h, len(cfg.data): u32);
	hash::write(&h, cfg.data);

	hash::sum(&h, dest[..]);
};

fn hash_leputu32(h: *hash::hash, u: u32) void = {
	let buf: [4]u8 = [0...];
	endian::leputu32(buf, u);
	hash::write(h, buf[..]);
};

// The variable hash function H'
fn varhash(dest: []u8, block: []u8) void = {
	let u32buf: [4]u8 = [0...];

	if (len(dest) <= 64) {
		let h = blake2b::blake2b([], len(dest));
		defer hash::close(&h);
		hash_leputu32(&h, len(dest): u32);
		hash::write(&h, block);
		hash::sum(&h, dest);
		return;
	};

	// TODO this may be replaced with a constant time divceil in future to
	// avoid leaking the dest len.
	const r = divceil(len(dest): u32, 32) - 2;
	let v: [64]u8 = [0...];

	let destbuf = bufio::fixed(dest, io::mode::WRITE);

	let h = blake2b::blake2b([], 64);
	hash_leputu32(&h, len(dest): u32);
	hash::write(&h, block);
	hash::sum(&h, v[..]);
	hash::close(&h);

	io::writeall(&destbuf, v[..32])!;

	for (let i = 1z; i < r; i += 1) {
		let h = blake2b::blake2b([], 64);
		hash::write(&h, v[..]);
		hash::sum(&h, v[..]);
		hash::close(&h);
		io::writeall(&destbuf, v[..32])!;
	};

	const remainder = len(dest) - 32 * r;
	let hend = blake2b::blake2b([], remainder);
	defer hash::close(&hend);
	hash::write(&hend, v[..]);
	hash::sum(&hend, v[..remainder]);
	io::writeall(&destbuf, v[..remainder])!;
};

fn divceil(dividend: u32, divisor: u32) u32 = {
	let result = dividend / divisor;
	if (dividend % divisor > 0) {
		result += 1;
	};
	return result;
};

fn xorblock(dest: []u64, x: []u64, y: []u64) void = {
	math::xor((dest: *[*]u8)[..len(dest) * size(u64)],
		(x: *[*]u8)[..len(dest) * size(u64)],
		(y: *[*]u8)[..len(dest) * size(u64)]);
};

fn segproc(cfg: *config, ctx: *context, i: size, slice: size) void = {
	const init = switch (ctx.mode) {
	case mode::I =>
		yield true;
	case mode::ID =>
		yield ctx.pass == 0 && slice <= 2;
	case mode::D =>
		yield false;
	};
	if (init) {
		ctx.seedsinit[0] = ctx.pass;
		ctx.seedsinit[1] = i;
		ctx.seedsinit[2] = slice;
		ctx.seedsinit[3] = len(ctx.mem) / BLOCKSIZE;
		ctx.seedsinit[4] = cfg.passes;
		ctx.seedsinit[5] = ctx.mode: u64;
		ctx.seedsinit[6] = 0;

		if (ctx.pass == 0 && slice == 0) {
			ctx.seedsinit[6] += 1;
			compress(ctx.seedblock, ctx.seedsinit, zeroblock,
				false);
			compress(ctx.seedblock, ctx.seedblock, zeroblock,
				false);
		};
	};

	for (let b = 0z; b < ctx.sliceblocks; b += 1) {
		const j = slice * ctx.sliceblocks + b;
		if (ctx.pass == 0 && j < 2) {
			continue;
		};

		const dmodeseed = switch (ctx.mode) {
		case mode::D =>
			yield true;
		case mode::ID =>
			yield ctx.pass > 0 || slice > 1;
		case mode::I =>
			yield false;
		};
		const seed: u64 = if (dmodeseed) {
			yield block(ctx, i, (j - 1) % ctx.cols)[0];
		} else {
			if (b % BLOCKSIZE == 0) {
				ctx.seedsinit[6] += 1;
				compress(ctx.seedblock, ctx.seedsinit,
					zeroblock, false);
				compress(ctx.seedblock, ctx.seedblock,
					zeroblock, false);
			};
			yield ctx.seedblock[b % BLOCKSIZE];
		};
		compress(block(ctx, i, j), block(ctx, i, (j - 1) % ctx.cols),
				refblock(cfg, ctx, seed, i, j), ctx.pass > 0);
	};
};

fn compress(dest: []u64, x: []u64, y: []u64, xor: bool) void = {
	let r: block64 = [0...];
	xorblock(r, x, y);

	let z: block64 = [0...];
	z[..] = r[..];

	for (let i = 0z; i < 128; i += 16) {
		perm(&z[i], &z[i + 1], &z[i + 2], &z[i + 3], &z[i + 4],
			&z[i + 5], &z[i + 6], &z[i + 7], &z[i + 8], &z[i + 9],
			&z[i + 10], &z[i + 11], &z[i + 12], &z[i + 13],
			&z[i + 14], &z[i + 15]);
	};

	for (let i = 0z; i < 16; i += 2) {
		perm(&z[i], &z[i + 1], &z[i + 16], &z[i + 17], &z[i + 32],
			&z[i + 33], &z[i + 48], &z[i + 49], &z[i + 64],
			&z[i + 65], &z[i + 80], &z[i + 81], &z[i + 96],
			&z[i + 97], &z[i + 112], &z[i + 113]);
	};

	if (xor) {
		xorblock(r, r, dest);
	};

	xorblock(dest, z, r);
};

fn perm(
	x0: *u64,
	x1: *u64,
	x2: *u64,
	x3: *u64,
	x4: *u64,
	x5: *u64,
	x6: *u64,
	x7: *u64,
	x8: *u64,
	x9: *u64,
	x10: *u64,
	x11: *u64,
	x12: *u64,
	x13: *u64,
	x14: *u64,
	x15: *u64,
) void = {
	mix(x0, x4, x8, x12);
	mix(x1, x5, x9, x13);
	mix(x2, x6, x10, x14);
	mix(x3, x7, x11, x15);

	mix(x0, x5, x10, x15);
	mix(x1, x6, x11, x12);
	mix(x2, x7, x8, x13);
	mix(x3, x4, x9, x14);
};

fn mix(a: *u64, b: *u64, c: *u64, d: *u64) void = {
	*a = *a + *b + 2 * trunc(*a) * trunc(*b);
	*d = math::rotr64(*d ^ *a, 32);
	*c = *c + *d + 2 * trunc(*c) * trunc(*d);
	*b = math::rotr64(*b ^ *c, 24);

	*a = *a + *b + 2 * trunc(*a) * trunc(*b);
	*d = math::rotr64(*d ^ *a, 16);
	*c = *c + *d + 2 * trunc(*c) * trunc(*d);
	*b = math::rotr64(*b ^ *c, 63);
};

fn trunc(a: u64) u64 = {
	return a & 0xffffffff;
};
diff --git a/secstore/secstore.ha b/secstore/secstore.ha
index d01eb0f..50b8a9a 100644
--- a/secstore/secstore.ha
+++ b/secstore/secstore.ha
@@ -1,6 +1,7 @@
use bufio;
use bytes;
use crypto::argon2;
use crypto::argon2bug;
use crypto::keystore;
use crypto::random;
use crypto;
@@ -232,7 +233,11 @@ export fn unlock(store: *secstore, passphrase: []u8) (void | error) = {
	};
	switch (items[0]) {
	case "argon2i" =>
		argon2::argon2i(key, passphrase, salt, &config)!;
		if (kv == keyversion::V0) {
			argon2bug::argon2i(key, passphrase, salt, &config)!;
		} else {
			argon2::argon2i(key, passphrase, salt, &config)!;
		};
	case "argon2id" =>
		argon2::argon2id(key, passphrase, salt, &config)!;
	case =>
-- 
2.40.0

[PATCH himitsu v2 4/5] add re-encryption support

Details
Message ID
<20230423080713.1986-4-apreiml@strohwolke.at>
In-Reply-To
<20230423080713.1986-1-apreiml@strohwolke.at> (view parent)
DKIM signature
missing
Download raw message
Patch: +115 -3
Tested-by: Sam Nystrom <sam@samnystrom.dev>
---
 cmd/himitsu-init/main.ha | 52 +++++++++++++++++++++++++++++++
 secstore/secstore.ha     | 66 ++++++++++++++++++++++++++++++++++++++--
 2 files changed, 115 insertions(+), 3 deletions(-)

diff --git a/cmd/himitsu-init/main.ha b/cmd/himitsu-init/main.ha
index 0989c6d..39f7f56 100644
--- a/cmd/himitsu-init/main.ha
+++ b/cmd/himitsu-init/main.ha
@@ -10,12 +10,29 @@ use path;
use secstore;
use strings;
use unix::tty;
use getopt;

// XXX: Distros may want to modify the default config
const conf: str = `[himitsud]
prompter=hiprompt-gtk`;

export fn main() void = {

	const cmd = getopt::parse(os::args,
		"himitsu-init",
		('r', "Change master key and re-encrypt the secstore."),
	);
	defer getopt::finish(&cmd);

	let re = false;
	for (let i = 0z; i < len(cmd.opts); i += 1) {
		const opt = cmd.opts[i];
		switch (opt.0) {
		case 'r' =>
			re = true;
		};
	};

	const tty = match (tty::open()) {
	case let file: io::file =>
		yield file;
@@ -32,6 +49,11 @@ export fn main() void = {
	const tty = &bufio::buffered(tty, rbuf, wbuf);
	defer io::close(tty)!;

	if (re) {
		reencrypt(tty);
		return;
	};

	// TODO: Prompt before overwriting existing secstore
	fmt::errorln("Initializing a new himitsu secstore.")!;
	fmt::error("Please enter a passphrase: ")!;
@@ -103,3 +125,33 @@ fn writeconf() void = {

	fmt::println("Wrote config file to", confpath)!;
};

fn reencrypt(tty: io::handle) void = {
	fmt::errorln("Re-encrypting the secstore.\n")!;
	fmt::errorln("There are not many safeguards in effect, so please read carefully.")!;
	fmt::errorln()!;
	fmt::errorfln(
		"Make sure that no himitsud process is currently running. The current\n"
		"secstore will be moved to himitsu.old in our data directory:\n\n"
		"\t{}\n\nIf the process fails in between, make sure that himitsu.new in the \n"
		"data directory is removed and try again.\n",
		dirs::data(""),
	)!;

	fmt::error("Please enter your passphrase: ")!;
	const pass = match (bufio::scanline(tty)!) {
	case let buf: []u8 =>
		yield buf;
	case io::EOF =>
		fmt::fatal("Error: no passphrase supplied");
	};
	defer free(pass);
	fmt::errorln()!;

	secstore::reencrypt(pass)!;

	fmt::errorln(
		"\nSecstore has been re-encrypted. If everything works,you may remove the\n"
		"himitsu.old folder from your data directory"
	)!;
};
diff --git a/secstore/secstore.ha b/secstore/secstore.ha
index 50b8a9a..76419e8 100644
--- a/secstore/secstore.ha
+++ b/secstore/secstore.ha
@@ -33,6 +33,11 @@ type keyversion = enum u8 {
// Initializes a new secstore using the provided passphrase. The caller should
// call [[close]] when they're done with it.
export fn create(passphrase: []u8) (secstore | error) = {
	let dir = strings::dup(dirs::data("himitsu"));
	return createat(passphrase, dir);
};

fn createat(passphrase: []u8, dir: const str) (secstore | error) = {
	let key: [32 + 16]u8 = [0...];
	defer bytes::zero(key);

@@ -48,7 +53,6 @@ export fn create(passphrase: []u8) (secstore | error) = {
	argon2::argon2id(key, passphrase, salt, &config)!;

	const verify = key[32..];
	const dir = strings::dup(dirs::data("himitsu"));

	let buf = path::init();
	path::set(&buf, dir, "key")!;
@@ -67,7 +71,7 @@ export fn create(passphrase: []u8) (secstore | error) = {
	let masterkeybuf: [32]u8 = [0...];
	random::buffer(masterkeybuf);
	defer bytes::zero(masterkeybuf);
	let masterkey = keystore::newkey(masterkeybuf, "secstore")!;
	let masterkey = keystore::newkey(masterkeybuf, "secstoreinit")!;

	let mkkey = keystore::newkey(key[..32], "masterkeykey")!;
	defer keystore::destroy(mkkey);
@@ -88,6 +92,58 @@ export fn create(passphrase: []u8) (secstore | error) = {
	};
};

// Generates a new master key and re-encrypts the secstore.
export fn reencrypt(passphrase: []u8) (secstore | error) = {
	let dir = strings::dup(dirs::data("himitsu"));
	let dirnew = strings::dup(dirs::data("himitsu.new"));

	let oldstore = openat(dir)?;
	unlock(&oldstore, passphrase)?;
	defer close(&oldstore);

	let store = createat(passphrase, dirnew)!;
	defer close(&store);

	const q = query::query { ... };
	const iter = query(&oldstore, &q);
	for (true) {
		const item = match (next(&oldstore, &iter)) {
		case let item: *entry =>
			let buf = bufio::dynamic(io::mode::RDWR);
			defer io::close(&buf)!;
			write(&oldstore, &buf, item, true)!;

			io::seek(&buf, 0, io::whence::SET)!;

			let iq = query::parse(&buf)!;
			defer query::finish(&iq);
			add(&store, &iq)!;
			yield item;
		case void =>
			break;
		};
	};

	let dirold = strings::dup(dirs::data("himitsu.old"));
	match (os::rename(dir, dirold)) {
	case let e: fs::error =>
		fmt::fatalf("Moving himitsu to himitsu.old failed: {}",
			fs::strerror(e));
	case =>
		yield;
	};

	match (os::rename(dirnew, dir)) {
	case let e: fs::error =>
		fmt::fatalf("Moving himitsu.new to himitsu failed: {}",
			fs::strerror(e));
	case =>
		yield;
	};

	return store;
};

// Encrypts msg and writes it to sink
fn secbox(
	sink: io::handle,
@@ -137,7 +193,11 @@ fn secunbox(src: io::handle, key: (keystore::key | []u8)) ([]u8 | io::error) = {
// Opens the secstore. The caller should call [[unlock]] to decrypt it, and
// [[close]] when they're done with it.
export fn open() (secstore | error) = {
	const dir = strings::dup(dirs::data("himitsu"));
	let dir = strings::dup(dirs::data("himitsu"));
	return openat(dir);
};

fn openat(dir: str) (secstore | error) = {
	let buf = path::init();
	path::set(&buf, dir, "index")!;

-- 
2.40.0

[PATCH himitsu v2 5/5] vendor fixed argon2 module

Details
Message ID
<20230423080713.1986-5-apreiml@strohwolke.at>
In-Reply-To
<20230423080713.1986-1-apreiml@strohwolke.at> (view parent)
DKIM signature
missing
Download raw message
Patch: +797 -0
Vendor the fixed argon2 module also for a while to make sure the new keys will
be derived properly, if the new himitsu version is accidently build with an
outdated hare stack.
---
 crypto/argon2/+test.ha  | 246 +++++++++++++++++++
 crypto/argon2/README    |  30 +++
 crypto/argon2/argon2.ha | 521 ++++++++++++++++++++++++++++++++++++++++
 3 files changed, 797 insertions(+)
 create mode 100644 crypto/argon2/+test.ha
 create mode 100644 crypto/argon2/README
 create mode 100644 crypto/argon2/argon2.ha

diff --git a/crypto/argon2/+test.ha b/crypto/argon2/+test.ha
new file mode 100644
index 0000000..aa2b3eb
--- /dev/null
+++ b/crypto/argon2/+test.ha
@@ -0,0 +1,246 @@
// License: MPL-2.0
// (c) 2021-2022 Armin Preiml <apreiml@strohwolke.at>
// (c) 2022 Drew DeVault <sir@cmpwn.com>
use bytes;
use encoding::hex;
use strings;

@test fn mode_d_one_pass() void = {
	let pass: [32]u8 = [1...];
	let salt: [16]u8 = [2...];
	let secret: [8]u8 = [3...];
	let data: [12]u8 = [4...];
	let result: [32]u8 = [0...];

	let expected: [_]u8 = [
		0xfa, 0x17, 0x75, 0xca, 0x80, 0x90, 0x64, 0x66, 0x18, 0xbe,
		0x70, 0xeb, 0x0f, 0xc9, 0xde, 0x43, 0x67, 0x58, 0xed, 0x0c,
		0xa5, 0x36, 0x83, 0x1a, 0xe9, 0xe1, 0x03, 0x48, 0x93, 0x81,
		0xc1, 0x79,
	];

	let cfg = config {
		secret = secret,
		data = data,
		passes = 1,
		parallel = 4,
		version = 0x13,
		mem = 32,
		...
	};

	argon2d(result[..], pass, salt, &cfg)!;

	assert(bytes::equal(result, expected));
};

@test fn rfc_d_test_vector() void = {
	let pass: [32]u8 = [1...];
	let salt: [16]u8 = [2...];
	let secret: [8]u8 = [3...];
	let data: [12]u8 = [4...];
	let result: [32]u8 = [0...];

	let mem: []u64 = alloc([0...], 32z * BLOCKSIZE);
	defer free(mem);

	let expected: [_]u8 = [
		0x51, 0x2b, 0x39, 0x1b, 0x6f, 0x11, 0x62, 0x97, 0x53, 0x71,
		0xd3, 0x09, 0x19, 0x73, 0x42, 0x94, 0xf8, 0x68, 0xe3, 0xbe,
		0x39, 0x84, 0xf3, 0xc1, 0xa1, 0x3a, 0x4d, 0xb9, 0xfa, 0xbe,
		0x4a, 0xcb,
	];

	let cfg = config {
		secret = secret,
		data = data,
		passes = 3,
		parallel = 4,
		version = 0x13,
		mem = mem[..],
		...
	};

	argon2d(result[..], pass, salt, &cfg)!;

	assert(bytes::equal(result, expected));
};


@test fn rfc_i_test_vector() void = {
	let pass: [32]u8 = [1...];
	let salt: [16]u8 = [2...];
	let secret: [8]u8 = [3...];
	let data: [12]u8 = [4...];
	let result: [32]u8 = [0...];

	let mem: []u64 = alloc([0...], 32z * BLOCKSIZE);
	defer free(mem);

	let expected: [_]u8 = [
		0xc8, 0x14, 0xd9, 0xd1, 0xdc, 0x7f, 0x37, 0xaa, 0x13, 0xf0,
		0xd7, 0x7f, 0x24, 0x94, 0xbd, 0xa1, 0xc8, 0xde, 0x6b, 0x01,
		0x6d, 0xd3, 0x88, 0xd2, 0x99, 0x52, 0xa4, 0xc4, 0x67, 0x2b,
		0x6c, 0xe8,
	];

	let cfg = config {
		secret = secret,
		data = data,
		passes = 3,
		parallel = 4,
		version = 0x13,
		mem = mem[..],
		...
	};

	argon2i(result[..], pass, salt, &cfg)!;

	assert(bytes::equal(result, expected));
};

@test fn rfc_id_test_vector() void = {
	let pass: [32]u8 = [1...];
	let salt: [16]u8 = [2...];
	let secret: [8]u8 = [3...];
	let data: [12]u8 = [4...];
	let result: [32]u8 = [0...];

	let mem: []u64 = alloc([0...], 32z * BLOCKSIZE);
	defer free(mem);

	let expected: [_]u8 = [
		0x0d, 0x64, 0x0d, 0xf5, 0x8d, 0x78, 0x76, 0x6c, 0x08, 0xc0,
		0x37, 0xa3, 0x4a, 0x8b, 0x53, 0xc9, 0xd0, 0x1e, 0xf0, 0x45,
		0x2d, 0x75, 0xb6, 0x5e, 0xb5, 0x25, 0x20, 0xe9, 0x6b, 0x01,
		0xe6, 0x59,
	];

	let cfg = config {
		secret = secret,
		data = data,
		passes = 3,
		parallel = 4,
		version = 0x13,
		mem = mem[..],
		...
	};

	argon2id(result[..], pass, salt, &cfg)!;

	assert(bytes::equal(result, expected));
};

type tcase = struct {
	c: config,
	m: mode,
	h: str,
};

@test fn samples() void = {
	const pass = strings::toutf8("trustno1");
	const salt = strings::toutf8("abcdefgh");

	const tests: [_]tcase = [
		// XXX disabled for now because it's slow
		// tcase {
		// 	c = low_mem_config,
		// 	m = mode::ID,
		// 	h = "8974537c53677aae532b319af700bb4232a0d74eee7d57296b2a3f8303a6bafe",
		// },
		// tcase {
		// 	c = default_config,
		// 	m = mode::ID,
		// 	h = "3b282cbf435b0e022f7041549583ddc802e519109f1da8f12d2054910913d660",
		// },
		tcase {
			c = config {
				passes = 1,
				parallel = 3,
				version = 0x13,
				mem = 64,
				...
			},
			m = mode::ID,
			h = "c7ada5ba3222fa45a3802249b509dcfb10e68a50e3faad2a6377eeca8395ab47",
		},
		tcase {
			c = config {
				passes = 1,
				parallel = 4,
				version = 0x13,
				mem = 64,
				...
			},
			m = mode::ID,
			h = "21543b2017ede3f865ea5cb88295628ba25eb3be53a8c4aeb0ac1a264be0110a",
		},
		tcase {
			c = config {
				passes = 1,
				parallel = 4,
				version = 0x13,
				mem = 64,
				...
			},
			m = mode::I,
			h = "5c3124ce5f3556e5e25f06b5108718f2cd72afee98a3249656eb85ecc0e5b314",
		},
		tcase {
			c = config {
				passes = 1,
				parallel = 4,
				version = 0x13,
				mem = 64,
				...
			},
			m = mode::D,
			h = "d75524ad0b899363ce77f2d1e1040763dc01cfc725db635391bba163001f08cb",
		},
		tcase {
			c = config {
				passes = 3,
				parallel = 3,
				version = 0x13,
				mem = 64,
				...
			},
			m = mode::ID,
			h = "226c3ca6caba42b102035d332a11b350f1e19675fccb6e24aa33ca8c31d588c1",
		},
		tcase {
			c = config {
				passes = 1,
				parallel = 8,
				version = 0x13,
				mem = 64,
				...
			},
			m = mode::ID,
			h = "fadf598b70708f4d91b0e98f038fd25a73950f1f85d57fb250740d817f95e9a9",
		},
		tcase {
			c = config {
				passes = 1,
				parallel = 4,
				version = 0x13,
				mem = 96,
				...
			},
			m = mode::ID,
			h = "c99aa41cb53cc4919d336c19d38b30d8633c71faa9475293f3fbe0aa6ccd65b2",
		},
	];

	for (let i = 0z; i < len(tests); i += 1) {
		const t = tests[i];
		const expected = hex::decodestr(t.h)!;
		defer free(expected);
		let dest: []u8 = alloc([0...], len(expected));
		defer free(dest);

		argon2(dest, pass, salt, &t.c, t.m)!;
		assert(bytes::equal(expected, dest));
	};
};

diff --git a/crypto/argon2/README b/crypto/argon2/README
new file mode 100644
index 0000000..b06abdf
--- /dev/null
+++ b/crypto/argon2/README
@@ -0,0 +1,30 @@
This module provides an implementation of the argon2 key derivation function as
described by RFC 9106. This is the recommended algorithm for password hashing in
Hare programs, and for deriving keys for use with other cryptographic
algorithms. Some thought must be given to the appropriate configuration for your
use case. Some general advice is provided here; if in doubt, consult the RFC.

The argon2 parameters are configured via the [[config]] structure. To determine
the appropriate configuration parameters for a particular use-case, consult
section 4 of the RFC. Otherwise, sane defaults for common scenarios are provided
via [[default_config]] and [[low_mem_config]]; consult the docs of each
configuration for details.

Once a suitable configuration has been selected, the user must provide a salt.
This salt should be stored alongside the hash, should be unique for each
password, and should be random: see [[crypto::random]]. The salt and hash
lengths are configurable, the recommended defaults are 16 and 32 bytes
respectively.

Equipped with the necessary parameters, the user may call the appropriate argon2
variant via [[argon2d]], [[argon2i]], or [[argon2id]]. If unsure which to use,
choose [[argon2id]]. The RFC is the authoratative source on the appropriate
argon2 variant and configuration parameters for your use-case.

This is a low-level module which implements cryptographic primitives. Direct use
of cryptographic primitives is not recommended for non-experts, as incorrect use
of these primitives can easily lead to the introduction of security
vulnerabilities. Non-experts are advised to use the high-level operations
available in the top-level [[crypto]] module.

Be advised that Hare's cryptography implementations have not been audited.
diff --git a/crypto/argon2/argon2.ha b/crypto/argon2/argon2.ha
new file mode 100644
index 0000000..a9c50fa
--- /dev/null
+++ b/crypto/argon2/argon2.ha
@@ -0,0 +1,521 @@
// (c) 2022 Alexey Yerin <yyp@disroot.org>
// (c) 2021-2022 Armin Preiml <apreiml@strohwolke.at>
// (c) 2021-2022 Drew DeVault <sir@cmpwn.com>
use bufio;
use bytes;
use crypto::blake2b;
use crypto::math;
use endian;
use errors::{nomem};
use hash;
use io;
use types;

// Latest version of argon2 supported by this implementation (1.3).
export def VERSION: u8 = 0x13;

// Number of u64 elements of one block.
export def BLOCKSIZE: u32 = 128;

def SLICES: size = 4;

type block64 = [BLOCKSIZE]u64;

const zeroblock: block64 = [0...];

type mode = enum {
	D = 0,
	I = 1,
	ID = 2,
};

// This type provides configuration options for the argon2 algorithm. Most users
// will find [[default_config]] or [[low_mem_config]] suitable for their needs
// without providing a custom configuration. If writing a custom configuration,
// consult the RFC for advice on selecting suitable values for your use-case.
//
// 'parallel' specifies the number of parallel processes. 'pass' configures the
// number of iterations. Both values must be at least one. Note: the Hare
// implementation of argon2 does not process hashes in parallel, though it will
// still compute the correct hash if this value is greater than one.
//
// 'version' specifies the version of the argon2 function. The implementation
// currently only supports version 1.3. Use [[VERSION]] here.
//
// 'secret' and 'data' are optional byte arrays that are applied to the initial
// state. Consult the RFC for details.
//
// The 'mem' parameter is used to configure working memory used during the
// computation. The argon2 algorithm requires a large amount of memory to
// compute hashes. If 'mem' set to a u32, it is interpreted as the desired
// number of 1024-byte blocks the implementation shall allocate for you. If the
// caller wants to manage the allocation itself, provide a []u8 instead. The
// length of this slice must be at least 8 times the value of 'parallel' in
// blocks, and must be a multiple of [[BLOCKSIZE]]. To have the implementation
// allocate 64 KiB, set 'mem' to 64. To use the same amount of caller-provided
// memory, provide a slice of length 64 * [[BLOCKSIZE]].
export type config = struct {
	mem: (u32 | []u64),
	parallel: u32,
	passes: u32,
	version: u8,
	secret: []u8,
	data: []u8
};

// The default recommended configuration for most use cases. This configuration
// uses 2 GiB of working memory. A 16-byte 'salt' and 32-byte 'dest' parameter
// is recommended in combination with this configuration.
export const default_config: config = config {
	mem = 2 * 1024 * 1024,
	passes = 1,
	parallel = 4,
	version = 0x13,
	...
};

// The default recommended configuration for memory-constrained use cases. This
// configuration uses 64 MiB of working memory. A 16-byte 'salt' and 32-byte
// 'dest' parameter is recommended in combination with this configuration.
export const low_mem_config: config = config {
	mem = 64 * 1024,
	passes = 3,
	parallel = 4,
	version = 0x13,
	...
};

type context = struct {
	mode: mode,
	cols: size,
	rows: size,
	sliceblocks: size,
	mem: []u64,
	pass: u32,
	seedsinit: block64,
	seedblock: block64,
};

// Computes an argon2d hash, writing the digest to 'dest'. A 'salt' length of 16
// bytes is recommended, and 8 bytes is the minimum. A 'dest' length of 32 bytes
// is recommended, and 4 bytes is the minimum.
//
// The argon2d mode uses data-dependent memory access and is suitable for
// applications with no threats of side-channel timing attacks.
export fn argon2d(
	dest: []u8,
	password: []u8,
	salt: []u8,
	cfg: *config,
) (void | nomem) = {
	return argon2(dest, password, salt, cfg, mode::D);
};

// Computes an argon2i hash, writing the digest to 'dest'. A 'salt' length of 16
// bytes is recommended, and 8 bytes is the minimum. A 'dest' length of 32 bytes
// is recommended, and 4 bytes is the minimum.
//
// The argon2i mode uses data-independent memory access and is suitable for
// password hashing and key derivation. It makes more passes over memory to
// protect from trade-off attacks.
export fn argon2i(
	dest: []u8,
	password: []u8,
	salt: []u8,
	cfg: *config,
) (void | nomem) = {
	return argon2(dest, password, salt, cfg, mode::I);
};

// Computes an argon2id hash, writing the digest to 'dest'. A 'salt' length of
// 16 bytes is recommended, and 8 bytes is the minimum. A 'dest' length of 32
// bytes is recommended, and 4 bytes is the minimum.
//
// The argon2id mode works by using argon2i for the first half of the first pass
// and argon2d further on. It provides therefore protection from side-channel
// attacks and brute-force cost savings due to memory trade-offs.
//
// If you are unsure which variant to use, argon2id is recommended.
export fn argon2id(
	dest: []u8,
	password: []u8,
	salt: []u8,
	cfg: *config,
) (void | nomem) = {
	return argon2(dest, password, salt, cfg, mode::ID);
};

fn argon2(
	dest: []u8,
	password: []u8,
	salt: []u8,
	cfg: *config,
	mode: mode,
) (void | nomem) = {
	assert(endian::host == &endian::little, "TODO big endian support");

	assert(len(dest) >= 4 && len(dest) <= types::U32_MAX);
	assert(len(password) <= types::U32_MAX);
	assert(len(salt) >= 8 && len(salt) <= types::U32_MAX);
	assert(cfg.parallel >= 1);
	assert(cfg.passes >= 1);
	assert(len(cfg.secret) <= types::U32_MAX);
	assert(len(cfg.data) <= types::U32_MAX);

	let initmemsize = 0u32;
	let mem: []u64 = match (cfg.mem) {
	case let mem: []u64 =>
		assert(len(mem) >= 8 * cfg.parallel * BLOCKSIZE
			&& len(mem) % BLOCKSIZE == 0
			&& len(mem) / BLOCKSIZE <= types::U32_MAX);
		initmemsize = (len(mem) / BLOCKSIZE): u32;

		// round down memory to nearest multiple of 4 times parallel
		const memsize = len(mem) - len(mem)
			% (4 * cfg.parallel * BLOCKSIZE);
		yield mem[..memsize];
	case let memsize: u32 =>
		assert(memsize >= 8 * cfg.parallel
			&& memsize <= types::U32_MAX);

		initmemsize = memsize;
		const memsize = memsize - memsize % (4 * cfg.parallel);
		yield alloc([0...], memsize * BLOCKSIZE): []u64;
	};

	let h0: [64]u8 = [0...];
	inithash(&h0, len(dest): u32, password, salt, cfg, mode, initmemsize);

	const memsize = (len(mem) / BLOCKSIZE): u32;
	const cols = 4 * (memsize / (4 * cfg.parallel));
	let ctx = context {
		rows = cfg.parallel,
		cols = cols,
		sliceblocks = cols / 4,
		pass = 0,
		mem = mem,
		mode = mode,
		seedsinit = [0...],
		seedblock = [0...],
		...
	};

	// hash first and second blocks of each row
	for (let i = 0z; i < ctx.rows; i += 1) {
		let src: [72]u8 = [0...];
		src[..64] = h0[..];

		endian::leputu32(src[64..68], 0);
		endian::leputu32(src[68..], i: u32);
		varhash(blocku8(&ctx, i, 0), src);

		endian::leputu32(src[64..68], 1);
		endian::leputu32(src[68..], i: u32);
		varhash(blocku8(&ctx, i, 1), src);
	};

	// process segments
	for (ctx.pass < cfg.passes; ctx.pass += 1) {
		for (let s = 0z; s < SLICES; s += 1) {
			for (let i = 0z; i < ctx.rows; i += 1) {
				segproc(cfg, &ctx, i, s);
			};
		};
	};

	// final hash
	let b = blocku8(&ctx, 0, ctx.cols - 1);
	for (let i = 1z; i < ctx.rows; i += 1) {
		math::xor(b, b, blocku8(&ctx, i, ctx.cols - 1));
	};

	varhash(dest, b);

	bytes::zero((h0: []u64: *[*]u8)[..len(h0) * size(u64)]);
	bytes::zero((ctx.mem: *[*]u8)[..len(ctx.mem) * size(u64)]);

	if (cfg.mem is u32) {
		// mem was allocated internally
		free(ctx.mem);
	};
};

fn block(ctx: *context, i: size, j: size) []u64 = {
	let index = (ctx.cols * i + j) * BLOCKSIZE;
	return ctx.mem[index..index + BLOCKSIZE];
};

fn blocku8(ctx: *context, i: size, j: size) []u8 = {
	return (block(ctx, i, j): *[*]u8)[..BLOCKSIZE * size(u64)];
};

fn refblock(cfg: *config, ctx: *context, seed: u64, i: size, j: size) []u64 = {
	const segstart = (j - (j % ctx.sliceblocks)) / ctx.sliceblocks;
	const index = j % ctx.sliceblocks;

	const l: size = if (segstart == 0 && ctx.pass == 0) {
		yield i;
	} else {
		yield (seed >> 32) % cfg.parallel;
	};

	let poolstart: u64 = ((segstart + 1) % SLICES) * ctx.sliceblocks;
	let poolsize: u64 = 3 * ctx.sliceblocks;

	if (i == l) {
		poolsize += index;
	};

	if (ctx.pass == 0) {
		poolstart = 0;
		poolsize = segstart * ctx.sliceblocks;
		if (segstart == 0 || i == l) {
			poolsize += index;
		};
	};

	if (index == 0 || i == l) {
		poolsize -= 1;
	};

	const j1: u64 = seed & 0xffffffff;
	const x: u64 = (j1 * j1) >> 32;
	const y: u64 = (poolsize * x) >> 32;
	const z: u64 = (poolstart + poolsize - (y+1)) % ctx.cols: u64;

	return block(ctx, l: size, z: size);
};

fn inithash(
	dest: *[64]u8,
	taglen: u32,
	password: []u8,
	salt: []u8,
	cfg: *config,
	mode: mode,
	memsize: u32,
) void = {
	let u32buf: [4]u8 = [0...];
	let h = blake2b::blake2b([], 64);
	defer hash::close(&h);

	hash_leputu32(&h, cfg.parallel);
	hash_leputu32(&h, taglen);
	hash_leputu32(&h, memsize);
	hash_leputu32(&h, cfg.passes);
	hash_leputu32(&h, cfg.version);
	hash_leputu32(&h, mode: u32);
	hash_leputu32(&h, len(password): u32);
	hash::write(&h, password);

	hash_leputu32(&h, len(salt): u32);
	hash::write(&h, salt);

	hash_leputu32(&h, len(cfg.secret): u32);
	hash::write(&h, cfg.secret);

	hash_leputu32(&h, len(cfg.data): u32);
	hash::write(&h, cfg.data);

	hash::sum(&h, dest[..]);
};

fn hash_leputu32(h: *hash::hash, u: u32) void = {
	let buf: [4]u8 = [0...];
	endian::leputu32(buf, u);
	hash::write(h, buf[..]);
};

// The variable hash function H'
fn varhash(dest: []u8, block: []u8) void = {
	let u32buf: [4]u8 = [0...];

	if (len(dest) <= 64) {
		let h = blake2b::blake2b([], len(dest));
		defer hash::close(&h);
		hash_leputu32(&h, len(dest): u32);
		hash::write(&h, block);
		hash::sum(&h, dest);
		return;
	};

	// TODO this may be replaced with a constant time divceil in future to
	// avoid leaking the dest len.
	const r = divceil(len(dest): u32, 32) - 2;
	let v: [64]u8 = [0...];

	let destbuf = bufio::fixed(dest, io::mode::WRITE);

	let h = blake2b::blake2b([], 64);
	hash_leputu32(&h, len(dest): u32);
	hash::write(&h, block);
	hash::sum(&h, v[..]);
	hash::close(&h);

	io::writeall(&destbuf, v[..32])!;

	for (let i = 1z; i < r; i += 1) {
		let h = blake2b::blake2b([], 64);
		hash::write(&h, v[..]);
		hash::sum(&h, v[..]);
		hash::close(&h);
		io::writeall(&destbuf, v[..32])!;
	};

	const remainder = len(dest) - 32 * r;
	let hend = blake2b::blake2b([], remainder);
	defer hash::close(&hend);
	hash::write(&hend, v[..]);
	hash::sum(&hend, v[..remainder]);
	io::writeall(&destbuf, v[..remainder])!;
};

fn divceil(dividend: u32, divisor: u32) u32 = {
	let result = dividend / divisor;
	if (dividend % divisor > 0) {
		result += 1;
	};
	return result;
};

fn xorblock(dest: []u64, x: []u64, y: []u64) void = {
	math::xor((dest: *[*]u8)[..len(dest) * size(u64)],
		(x: *[*]u8)[..len(dest) * size(u64)],
		(y: *[*]u8)[..len(dest) * size(u64)]);
};

fn segproc(cfg: *config, ctx: *context, i: size, slice: size) void = {
	const init = switch (ctx.mode) {
	case mode::I =>
		yield true;
	case mode::ID =>
		yield ctx.pass == 0 && slice < 2;
	case mode::D =>
		yield false;
	};
	if (init) {
		ctx.seedsinit[0] = ctx.pass;
		ctx.seedsinit[1] = i;
		ctx.seedsinit[2] = slice;
		ctx.seedsinit[3] = len(ctx.mem) / BLOCKSIZE;
		ctx.seedsinit[4] = cfg.passes;
		ctx.seedsinit[5] = ctx.mode: u64;
		ctx.seedsinit[6] = 0;

		if (ctx.pass == 0 && slice == 0) {
			ctx.seedsinit[6] += 1;
			compress(ctx.seedblock, ctx.seedsinit, zeroblock,
				false);
			compress(ctx.seedblock, ctx.seedblock, zeroblock,
				false);
		};
	};

	for (let b = 0z; b < ctx.sliceblocks; b += 1) {
		const j = slice * ctx.sliceblocks + b;
		if (ctx.pass == 0 && j < 2) {
			continue;
		};

		const dmodeseed = switch (ctx.mode) {
		case mode::D =>
			yield true;
		case mode::ID =>
			yield ctx.pass > 0 || slice > 1;
		case mode::I =>
			yield false;
		};

		const pj = if (j == 0) ctx.cols - 1 else j - 1;
		let prev = block(ctx, i, pj);

		const seed: u64 = if (dmodeseed) {
			yield prev[0];
		} else {
			if (b % BLOCKSIZE == 0) {
				ctx.seedsinit[6] += 1;
				compress(ctx.seedblock, ctx.seedsinit,
					zeroblock, false);
				compress(ctx.seedblock, ctx.seedblock,
					zeroblock, false);
			};
			yield ctx.seedblock[b % BLOCKSIZE];
		};

		let ref = refblock(cfg, ctx, seed, i, j);
		compress(block(ctx, i, j), prev, ref, ctx.pass > 0);
	};
};

fn compress(dest: []u64, x: []u64, y: []u64, xor: bool) void = {
	let r: block64 = [0...];
	xorblock(r, x, y);

	let z: block64 = [0...];
	z[..] = r[..];

	for (let i = 0z; i < 128; i += 16) {
		perm(&z[i], &z[i + 1], &z[i + 2], &z[i + 3], &z[i + 4],
			&z[i + 5], &z[i + 6], &z[i + 7], &z[i + 8], &z[i + 9],
			&z[i + 10], &z[i + 11], &z[i + 12], &z[i + 13],
			&z[i + 14], &z[i + 15]);
	};

	for (let i = 0z; i < 16; i += 2) {
		perm(&z[i], &z[i + 1], &z[i + 16], &z[i + 17], &z[i + 32],
			&z[i + 33], &z[i + 48], &z[i + 49], &z[i + 64],
			&z[i + 65], &z[i + 80], &z[i + 81], &z[i + 96],
			&z[i + 97], &z[i + 112], &z[i + 113]);
	};

	if (xor) {
		xorblock(r, r, dest);
	};

	xorblock(dest, z, r);
};

fn perm(
	x0: *u64,
	x1: *u64,
	x2: *u64,
	x3: *u64,
	x4: *u64,
	x5: *u64,
	x6: *u64,
	x7: *u64,
	x8: *u64,
	x9: *u64,
	x10: *u64,
	x11: *u64,
	x12: *u64,
	x13: *u64,
	x14: *u64,
	x15: *u64,
) void = {
	mix(x0, x4, x8, x12);
	mix(x1, x5, x9, x13);
	mix(x2, x6, x10, x14);
	mix(x3, x7, x11, x15);

	mix(x0, x5, x10, x15);
	mix(x1, x6, x11, x12);
	mix(x2, x7, x8, x13);
	mix(x3, x4, x9, x14);
};

fn mix(a: *u64, b: *u64, c: *u64, d: *u64) void = {
	*a = *a + *b + 2 * trunc(*a) * trunc(*b);
	*d = math::rotr64(*d ^ *a, 32);
	*c = *c + *d + 2 * trunc(*c) * trunc(*d);
	*b = math::rotr64(*b ^ *c, 24);

	*a = *a + *b + 2 * trunc(*a) * trunc(*b);
	*d = math::rotr64(*d ^ *a, 16);
	*c = *c + *d + 2 * trunc(*c) * trunc(*d);
	*b = math::rotr64(*b ^ *c, 63);
};

fn trunc(a: u64) u64 = {
	return a & 0xffffffff;
};
-- 
2.40.0

Re: [PATCH himitsu v2 5/5] vendor fixed argon2 module

Details
Message ID
<CS46AIX80YS5.1CV8RBLQQBWFS@pavilion>
In-Reply-To
<20230423080713.1986-5-apreiml@strohwolke.at> (view parent)
DKIM signature
missing
Download raw message
Reviewed-by: Sam Nystrom <sam@samnystrom.dev>
Reply to thread Export thread (mbox)