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 };