v / vlib / crypto / bcrypt / bcrypt.v
213 lines · 184 sloc · 5.24 KB · c968c9ec607ad85ef1f5735705c139e19f760cda
Raw
1module bcrypt
2
3import crypto.rand
4import crypto.blowfish
5
6pub const min_cost = 4
7pub const max_cost = 31
8pub const default_cost = 10
9pub const salt_length = 16
10pub const max_crypted_hash_size = 23
11pub const encoded_salt_size = 22
12pub const encoded_hash_size = 31
13pub const min_hash_size = 59
14
15pub const major_version = '2'
16pub const minor_version = 'a'
17
18const error_msg_max_length_exceed_72 = 'Maximum password length is 72 bytes'
19
20pub struct Hashed {
21mut:
22 hash []u8
23 salt []u8
24 cost int
25 major string
26 minor string
27}
28
29// free the resources taken by the Hashed `h`
30@[unsafe]
31pub fn (mut h Hashed) free() {
32 $if prealloc {
33 return
34 }
35 unsafe {
36 h.salt.free()
37 h.hash.free()
38 }
39}
40
41const magic_cipher_data = [u8(0x4f), 0x72, 0x70, 0x68, 0x65, 0x61, 0x6e, 0x42, 0x65, 0x68, 0x6f,
42 0x6c, 0x64, 0x65, 0x72, 0x53, 0x63, 0x72, 0x79, 0x44, 0x6f, 0x75, 0x62, 0x74]
43
44// generate_from_password return a bcrypt string from Hashed struct.
45pub fn generate_from_password(password []u8, cost int) !string {
46 if password.len > 72 {
47 return error(error_msg_max_length_exceed_72)
48 }
49 mut p := new_from_password(password, cost) or { return error('Error: ${err}') }
50 x := p.hash_u8()
51 return x.bytestr()
52}
53
54// compare_hash_and_password compares a bcrypt hashed password with its possible hashed version.
55pub fn compare_hash_and_password(password []u8, hashed_password []u8) ! {
56 if password.len > 72 {
57 return error(error_msg_max_length_exceed_72)
58 }
59 mut p := new_from_hash(hashed_password) or { return error('Error: ${err}') }
60 p.salt << `=`
61 p.salt << `=`
62 other_hash := bcrypt(password, p.cost, p.salt) or { return error('err') }
63 mut other_p := Hashed{
64 hash: other_hash
65 salt: p.salt
66 cost: p.cost
67 major: p.major
68 minor: p.minor
69 }
70
71 if p.hash_u8() != other_p.hash_u8() {
72 return error('mismatched hash and password')
73 }
74}
75
76// generate_salt generate a string to be treated as a salt.
77pub fn generate_salt() string {
78 randbytes := rand.bytes(salt_length) or { panic(err) }
79 return randbytes.bytestr()
80}
81
82// new_from_password converting from password to a Hashed struct with bcrypt.
83fn new_from_password(password []u8, cost int) !&Hashed {
84 mut cost_ := cost
85 if cost < min_cost {
86 cost_ = default_cost
87 }
88 mut p := &Hashed{}
89 p.major = major_version
90 p.minor = minor_version
91
92 if cost_ < min_cost || cost_ > max_cost {
93 return error('invalid cost')
94 }
95 p.cost = cost_
96
97 salt := generate_salt().bytes()
98 p.salt = base64_encode(salt).bytes()
99 hash := bcrypt(password, p.cost, p.salt) or { return err }
100 p.hash = hash
101 return p
102}
103
104// new_from_hash converting from hashed data to a Hashed struct.
105fn new_from_hash(hashed_secret []u8) !&Hashed {
106 mut tmp := hashed_secret.clone()
107 if tmp.len < min_hash_size {
108 return error('hash to short')
109 }
110
111 mut p := &Hashed{}
112 mut n := p.decode_version(tmp) or { return err }
113 tmp = tmp[n..].clone()
114
115 n = p.decode_cost(tmp) or { return err }
116 tmp = tmp[n..].clone()
117
118 p.salt = tmp[..encoded_salt_size].clone()
119 p.hash = tmp[encoded_salt_size..].clone()
120
121 return p
122}
123
124// bcrypt hashing passwords.
125fn bcrypt(password []u8, cost int, salt []u8) ![]u8 {
126 mut cipher_data := magic_cipher_data.clone()
127 mut bf := expensive_blowfish_setup(password, u32(cost), salt) or { return err }
128
129 for i := 0; i < 24; i += 8 {
130 for j := 0; j < 64; j++ {
131 bf.encrypt(mut cipher_data[i..i + 8], cipher_data[i..i + 8])
132 }
133 }
134 hash := base64_encode(cipher_data[..max_crypted_hash_size])
135 return hash.bytes()
136}
137
138// expensive_blowfish_setup generate a Blowfish cipher, given key, cost and salt.
139fn expensive_blowfish_setup(key []u8, cost u32, salt []u8) !&blowfish.Blowfish {
140 csalt := base64_decode(salt.bytestr())
141 // Bug compatibility with C bcrypt implementations, which use the trailing NULL in the key string during expansion.
142 // See https://cs.opensource.google/go/x/crypto/+/master:bcrypt/bcrypt.go;l=226
143 mut ckey := key.clone()
144 ckey << 0
145
146 mut bf := blowfish.new_salted_cipher(ckey, csalt) or { return err }
147
148 mut i := u64(0)
149 mut rounds := u64(0)
150 rounds = 1 << cost
151 for i = 0; i < rounds; i++ {
152 blowfish.expand_key(ckey, mut bf)
153 blowfish.expand_key(csalt, mut bf)
154 }
155
156 return &bf
157}
158
159// hash_byte converts the hash value to a byte array.
160fn (mut h Hashed) hash_u8() []u8 {
161 mut arr := []u8{len: 65, init: 0}
162 arr[0] = `$`
163 arr[1] = h.major[0]
164 mut n := 2
165 if h.minor != '0' {
166 arr[2] = h.minor[0]
167 n = 3
168 }
169 arr[n] = `$`
170 n++
171 copy(mut arr[n..], '${int(h.cost):02}'.bytes())
172 n += 2
173 arr[n] = `$`
174 n++
175 copy(mut arr[n..], h.salt)
176 n += encoded_salt_size
177 copy(mut arr[n..], h.hash)
178 n += encoded_hash_size
179 res := arr[..n].clone()
180 return res
181}
182
183// decode_version decode bcrypt version.
184fn (mut h Hashed) decode_version(sbytes []u8) !int {
185 if sbytes[0] != `$` {
186 return error("bcrypt hashes must start with '$'")
187 }
188 if sbytes[1] != major_version[0] {
189 return error('bcrypt algorithm version ${major_version}')
190 }
191 h.major = sbytes[1].ascii_str()
192 mut n := 3
193 if sbytes[2] != `$` {
194 h.minor = sbytes[2].ascii_str()
195 n++
196 }
197 return n
198}
199
200// decode_cost extracts the value of cost and returns the next index in the array.
201fn (mut h Hashed) decode_cost(sbytes []u8) !int {
202 cost := sbytes[0..2].bytestr().int()
203 check_cost(cost) or { return err }
204 h.cost = cost
205 return 3
206}
207
208// check_cost check for reasonable quantities.
209fn check_cost(cost int) ! {
210 if cost < min_cost || cost > max_cost {
211 return error('invalid cost')
212 }
213}
214