Armin Preiml: 5 move encryption into separate fn secstore: encrypted master key secstore: vendor buggy argon2 implementation add re-encryption support vendor fixed argon2 module 12 files changed, 1725 insertions(+), 71 deletions(-)
Reviewed-by: Sam Nystrom <sam@samnystrom.dev>
Copy & paste the following snippet into your terminal to import this patchset into git:
curl -s https://lists.sr.ht/~sircmpwn/himitsu-devel/patches/40596/mbox | git am -3Learn more about email & git
--- 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
--- 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
--- 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
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
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
Reviewed-by: Sam Nystrom <sam@samnystrom.dev>