hare

The Hare programming language
git clone https://git.torresjrjr.com/hare.git
Log | Files | Refs | README | LICENSE

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:
Acrypto/bcrypt/+test.ha | 19+++++++++++++++++++
Acrypto/bcrypt/base64.ha | 41+++++++++++++++++++++++++++++++++++++++++
Acrypto/bcrypt/bcrypt.ha | 208+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mscripts/gen-stdlib | 13+++++++++++++
Mstdlib.mk | 35+++++++++++++++++++++++++++++++++++
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 \