commit 2c77fdae3004757d3d8bbf348d9d30ce508dd3a2
parent 0499af1ede1150b52295750dccf512d3680ba62b
Author: Drew DeVault <sir@cmpwn.com>
Date: Wed, 27 Apr 2022 20:06:38 +0200
crypto::bcrypt: new module
This algorithm fucking sucks
Diffstat:
5 files changed, 316 insertions(+), 0 deletions(-)
diff --git a/crypto/bcrypt/+test.ha b/crypto/bcrypt/+test.ha
@@ -0,0 +1,19 @@
+use strings;
+use fmt;
+
+@test fn bcrypt() void = {
+ const pass = strings::toutf8("hare is cool");
+ const hash = generate(pass, DEFAULT_COST);
+ assert(compare(hash, pass)!);
+ const notpass = strings::toutf8("hare is lame");
+ assert(!compare(hash, notpass)!);
+};
+
+@test fn hash() void = {
+ const pass = strings::toutf8("allmine");
+ const salt = strings::toutf8("XajjQvNhvvRt5GSeFk1xFe");
+ const expect = "$2a$10$XajjQvNhvvRt5GSeFk1xFeyqRrsxkhBkUiQeg0dt.wU1qD4aFDcga";
+
+ const hash = strings::fromutf8(bcrypt(pass, salt, 10)!);
+ assert(strings::hassuffix(expect, hash));
+};
diff --git a/crypto/bcrypt/base64.ha b/crypto/bcrypt/base64.ha
@@ -0,0 +1,41 @@
+// License: MPL-2.0
+// (c) 2011 The Go Authors
+// (c) 2022 Drew DeVault <sir@cmpwn.com>
+//
+// bcrypt uses a crappy variant of base64 with its own special alphabet and no
+// padding. This file glues encoding::base64 to the bcrypt semantics.
+use bufio;
+use encoding::base64;
+use errors;
+use io;
+use fmt; // XXX: TEMP
+use strings;
+
+const alpha: str = "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+const b64encoding: base64::encoding = base64::encoding { ... };
+
+@init fn init() void = {
+ base64::encoding_init(&b64encoding, alpha);
+};
+
+// Encodes a slice in the bcrypt base64 style, returning a new slice. The caller
+// must free the return value.
+fn b64_encode(src: []u8) []u8 = {
+ let sink = bufio::dynamic(io::mode::WRITE);
+ base64::encode(&sink, &b64encoding, src)!;
+ let buf = bufio::buffer(&sink);
+ let i = len(buf);
+ for (i > 0 && buf[i - 1] == '='; i -= 1) void;
+ return buf[..i];
+};
+
+// Decodes a slice in the bcrypt base64 style, returning a new slice. The
+// caller must free the return value.
+fn b64_decode(src: []u8) ([]u8 | errors::invalid) = {
+ let src = alloc(src...);
+ defer free(src);
+ for (let neq = 4 - len(src) % 4; neq > 0; neq -= 1) {
+ append(src, '=');
+ };
+ return base64::decodeslice(&b64encoding, src);
+};
diff --git a/crypto/bcrypt/bcrypt.ha b/crypto/bcrypt/bcrypt.ha
@@ -0,0 +1,208 @@
+// License: MPL-2.0
+// (c) 2011 The Go Authors
+// (c) 2022 Drew DeVault <sir@cmpwn.com>
+//
+// This implementation is not great, but neither is this algorithm. Mostly
+// ported from Go.
+//
+// TODO: Move me into the extlib (hare-x-crypto?)
+use bufio;
+use bytes;
+use crypto::blowfish;
+use crypto::cipher;
+use crypto::random;
+use crypto;
+use errors;
+use fmt;
+use io;
+use strconv;
+use strings;
+
+let magic: []u8 = [
+ 0x4f, 0x72, 0x70, 0x68,
+ 0x65, 0x61, 0x6e, 0x42,
+ 0x65, 0x68, 0x6f, 0x6c,
+ 0x64, 0x65, 0x72, 0x53,
+ 0x63, 0x72, 0x79, 0x44,
+ 0x6f, 0x75, 0x62, 0x74,
+];
+
+type hash = struct {
+ hash: []u8,
+ salt: []u8,
+ cost: uint,
+ major: u8,
+ minor: u8,
+};
+
+// The minimum cost for a bcrypt hash.
+export def MIN_COST: uint = 4;
+
+// The maximum cost for a bcrypt hash.
+export def MAX_COST: uint = 32;
+
+// The recommended default cost for a bcrypt hash.
+export def DEFAULT_COST: uint = 10;
+
+def MAJOR: u8 = '2';
+def MINOR: u8 = 'a';
+def MAX_SALT_SIZE: size = 16;
+def MAX_CRYPTED_HASH_SIZE: size = 23;
+def ENCODED_SALT_SIZE: size = 22;
+def ENCODED_HASH_SIZE: size = 31;
+def MIN_HASH_SIZE: size = 59;
+
+// Hashes a password using the bcrypt algorithm. The caller must free the return
+// value.
+export fn generate(password: []u8, cost: uint) []u8 = {
+ let salt: [MAX_SALT_SIZE]u8 = [0...];
+ random::buffer(salt);
+ const hash = hash_password(password, salt, cost)!;
+ defer finish(&hash);
+ return mkhash(&hash);
+};
+
+// Compares a password against a bcrypt hash, returning true if the given
+// password matches the hash, or false otherwise. [[errors::invalid]] is
+// returned if the provided hash is not a valid bcrypt hash.
+export fn compare(hash: []u8, password: []u8) (bool | errors::invalid) = {
+ const hash = load(hash)?;
+ defer finish(&hash);
+ const salt = b64_decode(hash.salt)!;
+ defer free(salt);
+ const other = hash_password(password, salt, hash.cost)?;
+ defer finish(&other);
+ assert(hash.major == other.major);
+ assert(hash.minor == other.minor); // TODO?
+ return crypto::compare(hash.hash, other.hash);
+};
+
+fn mkhash(h: *hash) []u8 = {
+ let buf = bufio::dynamic(io::mode::WRITE);
+ fmt::fprintf(&buf, "${}$", h.major: u32: rune)!;
+ if (h.minor != 0) {
+ fmt::fprintf(&buf, "{}", h.minor: u32: rune)!;
+ };
+ fmt::fprintf(&buf, "${:02}$", h.cost)!;
+ io::write(&buf, h.salt)!;
+ io::write(&buf, h.hash)!;
+ return bufio::buffer(&buf);
+};
+
+fn hash_password(
+ password: []u8,
+ salt: []u8,
+ cost: uint,
+) (hash | errors::invalid) = {
+ assert(cost >= MIN_COST && cost <= MAX_COST, "Invalid bcrypt cost");
+ let hash = hash {
+ major = MAJOR,
+ minor = MINOR,
+ cost = cost,
+ ...
+ };
+ hash.salt = b64_encode(salt);
+ hash.hash = bcrypt(password, hash.salt, hash.cost)?;
+ return hash;
+};
+
+fn load(input: []u8) (hash | errors::invalid) = {
+ if (len(input) < MIN_HASH_SIZE || input[0] != '$') {
+ return errors::invalid;
+ };
+ let hash = hash { ... };
+
+ const tok = bytes::tokenize(input[1..], ['$']);
+
+ const major = loadtok(&tok)?;
+ hash.major = strings::toutf8(major)[0];
+ if (hash.major > MAJOR) {
+ return errors::invalid;
+ };
+
+ const minor = loadtok(&tok)?;
+ if (minor != "") {
+ hash.minor = strings::toutf8(minor)[0];
+ };
+
+ const cost = loadtok(&tok)?;
+ match (strconv::stou(cost)) {
+ case let u: uint =>
+ hash.cost = u;
+ case =>
+ return errors::invalid;
+ };
+
+ const data = strings::toutf8(loadtok(&tok)?);
+ if (!(loadtok(&tok) is errors::invalid)) {
+ return errors::invalid;
+ };
+
+ // XXX: There's some issue with alloc(data[..]...)
+ hash.salt = data[..ENCODED_SALT_SIZE];
+ hash.salt = alloc(hash.salt...);
+ hash.hash = data[ENCODED_SALT_SIZE..];
+ hash.hash = alloc(hash.hash...);
+ return hash;
+};
+
+fn loadtok(tok: *bytes::tokenizer) (str | errors::invalid) = {
+ match (bytes::next_token(tok)) {
+ case let b: []u8 =>
+ match (strings::try_fromutf8(b)) {
+ case let s: str =>
+ return s;
+ case =>
+ return errors::invalid;
+ };
+ case void =>
+ return errors::invalid;
+ };
+};
+
+fn bcrypt(password: []u8, salt: []u8, cost: uint) ([]u8 | errors::invalid) = {
+ let state: []u8 = alloc(magic...);
+ defer free(state);
+
+ let bf = expensive_blowfish(password, salt, cost)?;
+ for (let i = 0; i < 24; i += 8) {
+ for (let j = 0; j < 64; j += 1) {
+ cipher::encrypt(&bf, state[i..i+8], state[i..i+8]);
+ };
+ };
+
+ // Bug compat: only encode 32 bytes
+ const enc = b64_encode(state[..MAX_CRYPTED_HASH_SIZE]);
+ return enc;
+};
+
+fn expensive_blowfish(
+ key: []u8,
+ salt: []u8,
+ cost: uint,
+) (blowfish::state | errors::invalid) = {
+ let csalt = b64_decode(salt)?;
+ defer free(csalt);
+
+ // Bug compat: OpenBSD does this cool thing where it treats the string's
+ // trailing NUL as part of the key
+ let ckey = alloc(key...);
+ append(ckey, 0);
+ defer free(ckey);
+
+ const bf = blowfish::new();
+ blowfish::init_salt(&bf, ckey, csalt);
+
+ let rounds: u64 = 1u64 << cost;
+ for (let i = 0u64; i < rounds; i += 1) {
+ blowfish::init(&bf, ckey);
+ blowfish::init(&bf, csalt);
+ };
+
+ return bf;
+};
+
+fn finish(hash: *hash) void = {
+ free(hash.hash);
+ free(hash.salt);
+};
diff --git a/scripts/gen-stdlib b/scripts/gen-stdlib
@@ -268,6 +268,18 @@ crypto_argon2() {
crypto::math endian errors hash io rt types
}
+crypto_bcrypt() {
+ if [ $testing -eq 0 ]
+ then
+ gen_srcs crypto::bcrypt bcrypt.ha base64.ha
+ else
+ gen_srcs crypto::bcrypt bcrypt.ha base64.ha +test.ha
+ fi
+ gen_ssa crypto::bcrypt crypto::blowfish encoding::base64 bufio io \
+ crypto crypto::random errors crypto::cipher strings fmt bytes \
+ strconv
+}
+
gensrcs_crypto_blake2b() {
gen_srcs crypto::blake2b \
blake2b.ha \
@@ -1299,6 +1311,7 @@ crypto
crypto::aes
crypto::aes::xts
crypto::argon2
+crypto::bcrypt
crypto::blake2b
crypto::blowfish
crypto::chacha
diff --git a/stdlib.mk b/stdlib.mk
@@ -172,6 +172,12 @@ stdlib_deps_any += $(stdlib_crypto_argon2_any)
stdlib_crypto_argon2_linux = $(stdlib_crypto_argon2_any)
stdlib_crypto_argon2_freebsd = $(stdlib_crypto_argon2_any)
+# gen_lib crypto::bcrypt (any)
+stdlib_crypto_bcrypt_any = $(HARECACHE)/crypto/bcrypt/crypto_bcrypt-any.o
+stdlib_deps_any += $(stdlib_crypto_bcrypt_any)
+stdlib_crypto_bcrypt_linux = $(stdlib_crypto_bcrypt_any)
+stdlib_crypto_bcrypt_freebsd = $(stdlib_crypto_bcrypt_any)
+
# gen_lib crypto::blake2b (any)
stdlib_crypto_blake2b_any = $(HARECACHE)/crypto/blake2b/crypto_blake2b-any.o
stdlib_deps_any += $(stdlib_crypto_blake2b_any)
@@ -768,6 +774,17 @@ $(HARECACHE)/crypto/argon2/crypto_argon2-any.ssa: $(stdlib_crypto_argon2_any_src
@HARECACHE=$(HARECACHE) $(HAREC) $(HAREFLAGS) -o $@ -Ncrypto::argon2 \
-t$(HARECACHE)/crypto/argon2/crypto_argon2.td $(stdlib_crypto_argon2_any_srcs)
+# crypto::bcrypt (+any)
+stdlib_crypto_bcrypt_any_srcs = \
+ $(STDLIB)/crypto/bcrypt/bcrypt.ha \
+ $(STDLIB)/crypto/bcrypt/base64.ha
+
+$(HARECACHE)/crypto/bcrypt/crypto_bcrypt-any.ssa: $(stdlib_crypto_bcrypt_any_srcs) $(stdlib_rt) $(stdlib_crypto_blowfish_$(PLATFORM)) $(stdlib_encoding_base64_$(PLATFORM)) $(stdlib_bufio_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_crypto_$(PLATFORM)) $(stdlib_crypto_random_$(PLATFORM)) $(stdlib_errors_$(PLATFORM)) $(stdlib_crypto_cipher_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_fmt_$(PLATFORM)) $(stdlib_bytes_$(PLATFORM)) $(stdlib_strconv_$(PLATFORM))
+ @printf 'HAREC \t$@\n'
+ @mkdir -p $(HARECACHE)/crypto/bcrypt
+ @HARECACHE=$(HARECACHE) $(HAREC) $(HAREFLAGS) -o $@ -Ncrypto::bcrypt \
+ -t$(HARECACHE)/crypto/bcrypt/crypto_bcrypt.td $(stdlib_crypto_bcrypt_any_srcs)
+
# crypto::blake2b (+any)
stdlib_crypto_blake2b_any_srcs = \
$(STDLIB)/crypto/blake2b/blake2b.ha
@@ -2183,6 +2200,12 @@ testlib_deps_any += $(testlib_crypto_argon2_any)
testlib_crypto_argon2_linux = $(testlib_crypto_argon2_any)
testlib_crypto_argon2_freebsd = $(testlib_crypto_argon2_any)
+# gen_lib crypto::bcrypt (any)
+testlib_crypto_bcrypt_any = $(TESTCACHE)/crypto/bcrypt/crypto_bcrypt-any.o
+testlib_deps_any += $(testlib_crypto_bcrypt_any)
+testlib_crypto_bcrypt_linux = $(testlib_crypto_bcrypt_any)
+testlib_crypto_bcrypt_freebsd = $(testlib_crypto_bcrypt_any)
+
# gen_lib crypto::blake2b (any)
testlib_crypto_blake2b_any = $(TESTCACHE)/crypto/blake2b/crypto_blake2b-any.o
testlib_deps_any += $(testlib_crypto_blake2b_any)
@@ -2785,6 +2808,18 @@ $(TESTCACHE)/crypto/argon2/crypto_argon2-any.ssa: $(testlib_crypto_argon2_any_sr
@HARECACHE=$(TESTCACHE) $(HAREC) $(TESTHAREFLAGS) -o $@ -Ncrypto::argon2 \
-t$(TESTCACHE)/crypto/argon2/crypto_argon2.td $(testlib_crypto_argon2_any_srcs)
+# crypto::bcrypt (+any)
+testlib_crypto_bcrypt_any_srcs = \
+ $(STDLIB)/crypto/bcrypt/bcrypt.ha \
+ $(STDLIB)/crypto/bcrypt/base64.ha \
+ $(STDLIB)/crypto/bcrypt/+test.ha
+
+$(TESTCACHE)/crypto/bcrypt/crypto_bcrypt-any.ssa: $(testlib_crypto_bcrypt_any_srcs) $(testlib_rt) $(testlib_crypto_blowfish_$(PLATFORM)) $(testlib_encoding_base64_$(PLATFORM)) $(testlib_bufio_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_crypto_$(PLATFORM)) $(testlib_crypto_random_$(PLATFORM)) $(testlib_errors_$(PLATFORM)) $(testlib_crypto_cipher_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_fmt_$(PLATFORM)) $(testlib_bytes_$(PLATFORM)) $(testlib_strconv_$(PLATFORM))
+ @printf 'HAREC \t$@\n'
+ @mkdir -p $(TESTCACHE)/crypto/bcrypt
+ @HARECACHE=$(TESTCACHE) $(HAREC) $(TESTHAREFLAGS) -o $@ -Ncrypto::bcrypt \
+ -t$(TESTCACHE)/crypto/bcrypt/crypto_bcrypt.td $(testlib_crypto_bcrypt_any_srcs)
+
# crypto::blake2b (+any)
testlib_crypto_blake2b_any_srcs = \
$(STDLIB)/crypto/blake2b/blake2b.ha \