hare

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

bcrypt.ha (4792B)


      1 // SPDX-License-Identifier: MPL-2.0
      2 // (c) Hare authors <https://harelang.org>
      3 
      4 // This implementation is not great, but neither is this algorithm. Mostly
      5 // ported from Go.
      6 //
      7 // TODO: Move me into the extlib (hare-x-crypto?)
      8 use bytes;
      9 use crypto;
     10 use crypto::blowfish;
     11 use crypto::cipher;
     12 use crypto::random;
     13 use errors;
     14 use fmt;
     15 use io;
     16 use memio;
     17 use strconv;
     18 use strings;
     19 
     20 let magic: []u8 = [
     21 	0x4f, 0x72, 0x70, 0x68,
     22 	0x65, 0x61, 0x6e, 0x42,
     23 	0x65, 0x68, 0x6f, 0x6c,
     24 	0x64, 0x65, 0x72, 0x53,
     25 	0x63, 0x72, 0x79, 0x44,
     26 	0x6f, 0x75, 0x62, 0x74,
     27 ];
     28 
     29 type hash = struct {
     30 	hash: []u8,
     31 	salt: []u8,
     32 	cost: uint,
     33 	major: u8,
     34 	minor: u8,
     35 };
     36 
     37 // The minimum cost for a bcrypt hash.
     38 export def MIN_COST: uint = 4;
     39 
     40 // The maximum cost for a bcrypt hash.
     41 export def MAX_COST: uint = 32;
     42 
     43 // The recommended default cost for a bcrypt hash.
     44 export def DEFAULT_COST: uint = 10;
     45 
     46 def MAJOR: u8 = '2';
     47 def MINOR: u8 = 'a';
     48 def MAX_SALT_SZ: size = 16;
     49 def MAX_CRYPTED_HASH_SZ: size = 23;
     50 def ENCODED_SALT_SZ: size = 22;
     51 def ENCODED_HASH_SZ: size = 31;
     52 def MIN_HASH_SZ: size = 59;
     53 
     54 // Hashes a password using the bcrypt algorithm. The caller must free the return
     55 // value.
     56 export fn generate(password: []u8, cost: uint) []u8 = {
     57 	let salt: [MAX_SALT_SZ]u8 = [0...];
     58 	random::buffer(salt);
     59 	const hash = hash_password(password, salt, cost)!;
     60 	defer finish(&hash);
     61 	return mkhash(&hash);
     62 };
     63 
     64 // Compares a password against a bcrypt hash, returning true if the given
     65 // password matches the hash, or false otherwise. [[errors::invalid]] is
     66 // returned if the provided hash is not a valid bcrypt hash.
     67 export fn compare(hash: []u8, password: []u8) (bool | errors::invalid) = {
     68 	const hash = load(hash)?;
     69 	defer finish(&hash);
     70 	const salt = b64_decode(hash.salt)!;
     71 	defer free(salt);
     72 	const other = hash_password(password, salt, hash.cost)?;
     73 	defer finish(&other);
     74 	assert(hash.major == other.major);
     75 	assert(hash.minor == other.minor); // TODO?
     76 	return crypto::compare(hash.hash, other.hash);
     77 };
     78 
     79 fn mkhash(h: *hash) []u8 = {
     80 	let buf = memio::dynamic();
     81 	fmt::fprintf(&buf, "${}$", h.major: rune)!;
     82 	if (h.minor != 0) {
     83 		fmt::fprintf(&buf, "{}", h.minor: rune)!;
     84 	};
     85 	fmt::fprintf(&buf, "${:.2}$", h.cost)!;
     86 	io::write(&buf, h.salt)!;
     87 	io::write(&buf, h.hash)!;
     88 	return memio::buffer(&buf);
     89 };
     90 
     91 fn hash_password(
     92 	password: []u8,
     93 	salt: []u8,
     94 	cost: uint,
     95 ) (hash | errors::invalid) = {
     96 	assert(cost >= MIN_COST && cost <= MAX_COST, "Invalid bcrypt cost");
     97 	let hash = hash {
     98 		major = MAJOR,
     99 		minor = MINOR,
    100 		cost = cost,
    101 		...
    102 	};
    103 	hash.salt = b64_encode(salt);
    104 	hash.hash = bcrypt(password, hash.salt, hash.cost)?;
    105 	return hash;
    106 };
    107 
    108 fn load(input: []u8) (hash | errors::invalid) = {
    109 	if (len(input) < MIN_HASH_SZ || input[0] != '$') {
    110 		return errors::invalid;
    111 	};
    112 	let hash = hash { ... };
    113 
    114 	const tok = bytes::tokenize(input[1..], ['$']);
    115 
    116 	const major = loadtok(&tok)?;
    117 	hash.major = strings::toutf8(major)[0];
    118 	if (hash.major > MAJOR) {
    119 		return errors::invalid;
    120 	};
    121 
    122 	const minor = loadtok(&tok)?;
    123 	if (minor != "") {
    124 		hash.minor = strings::toutf8(minor)[0];
    125 	};
    126 
    127 	const cost = loadtok(&tok)?;
    128 	match (strconv::stou(cost)) {
    129 	case let u: uint =>
    130 		hash.cost = u;
    131 	case =>
    132 		return errors::invalid;
    133 	};
    134 
    135 	let data = strings::toutf8(loadtok(&tok)?);
    136 	if (!(loadtok(&tok) is errors::invalid)) {
    137 		return errors::invalid;
    138 	};
    139 
    140 	hash.salt = alloc(data[..ENCODED_SALT_SZ]...);
    141 	hash.hash = alloc(data[ENCODED_SALT_SZ..]...);
    142 	return hash;
    143 };
    144 
    145 fn loadtok(tok: *bytes::tokenizer) (str | errors::invalid) = {
    146 	match (bytes::next_token(tok)) {
    147 	case let b: []u8 =>
    148 		match (strings::fromutf8(b)) {
    149 		case let s: str =>
    150 			return s;
    151 		case =>
    152 			return errors::invalid;
    153 		};
    154 	case void =>
    155 		return errors::invalid;
    156 	};
    157 };
    158 
    159 fn bcrypt(password: []u8, salt: []u8, cost: uint) ([]u8 | errors::invalid) = {
    160 	let state: []u8 = alloc(magic...);
    161 	defer free(state);
    162 
    163 	let bf = expensive_blowfish(password, salt, cost)?;
    164 	defer free(bf);
    165 	for (let i = 0; i < 24; i += 8) {
    166 		for (let j = 0; j < 64; j += 1) {
    167 			cipher::encrypt(bf, state[i..i+8], state[i..i+8]);
    168 		};
    169 	};
    170 
    171 	// Bug compat: only encode 32 bytes
    172 	const enc = b64_encode(state[..MAX_CRYPTED_HASH_SZ]);
    173 	return enc;
    174 };
    175 
    176 fn expensive_blowfish(
    177 	key: []u8,
    178 	salt: []u8,
    179 	cost: uint,
    180 ) (*blowfish::state | errors::invalid) = {
    181 	let csalt = b64_decode(salt)?;
    182 	defer free(csalt);
    183 
    184 	// Bug compat: OpenBSD does this cool thing where it treats the string's
    185 	// trailing NUL as part of the key
    186 	let ckey = alloc(key...);
    187 	append(ckey, 0);
    188 	defer free(ckey);
    189 
    190 	const bf = alloc(blowfish::new());
    191 	blowfish::init_salt(bf, ckey, csalt);
    192 
    193 	let rounds: u64 = 1u64 << cost;
    194 	for (let i = 0u64; i < rounds; i += 1) {
    195 		blowfish::init(bf, ckey);
    196 		blowfish::init(bf, csalt);
    197 	};
    198 
    199 	return bf;
    200 };
    201 
    202 fn finish(hash: *hash) void = {
    203 	free(hash.hash);
    204 	free(hash.salt);
    205 };