This thread contains a patchset. You're looking at the original emails,
but you may wish to use the patch review UI.
Review patch
11
2
[PATCH himitsu 1/5] move encryption into separate fn
---
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 2/5] secstore: encrypted master key
---
secstore/secstore.ha | 88 ++++++++++++++++++++++++++++++++++++++ ------
1 file changed, 77 insertions(+), 11 deletions(-)
diff --git a/secstore/secstore.ha b/secstore/secstore.ha
index d4ca53a..6870d68 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,11 +169,11 @@ 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") {
+ if (items[0] != "argon2i" && items[0] != "argon2id") {
return badstore;
};
const mem = match (strconv::stou32(items[1])) {
@@ -191,10 +209,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 +233,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 =>
+ abort();
+ };
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 +265,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 3/5] secstore: vendor buggy argon2 implementation
---
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 6870d68..c22b465 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;
@@ -235,7 +236,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 4/5] add re-encryption support
---
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 c22b465..adf1571 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 5/5] vendor fixed argon2 module
---
We should 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 2/5] secstore: encrypted master key
On Sat Apr 22, 2023 at 7:59 AM EDT, Armin Preiml wrote:
> ---
> secstore/secstore.ha | 88 ++++++++++++++++++++++++++++++++++++++------
> 1 file changed, 77 insertions(+), 11 deletions(-)
>
> diff --git a/secstore/secstore.ha b/secstore/secstore.ha
> -%<-
> - 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 =>
> + abort();
The error handling should probably go here instead of the if statement
above.
Re: [PATCH himitsu 4/5] add re-encryption support
On Sat Apr 22, 2023 at 7:59 AM EDT, Armin Preiml wrote:
> ---
> 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)!) {
This should set noecho.
> + 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 c22b465..adf1571 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
Re: [PATCH himitsu 5/5] vendor fixed argon2 module
LGTM except for a few small issues.
Re: [PATCH himitsu 5/5] vendor fixed argon2 module
Re: [PATCH himitsu 4/5] add re-encryption support
On 4/23/23 01:33, Sam Nystrom wrote:
>> +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)!) {
>
> This should set noecho.
noecho is already set in main.
Re: [PATCH himitsu 4/5] add re-encryption support
On Sun Apr 23, 2023 at 2:39 AM EDT, Armin Preiml wrote:
> noecho is already set in main.
It wasn't when I tested it, the passphrase was echoed to the screen.
Re: [PATCH himitsu 4/5] add re-encryption support
My apologies, I applied the patch on top of my commit that moved the
noecho call down. You can disregard that comment.