
use utils

value BCRYPT_HASH_STRING_LEN (60)
value BCRYPT_MIN_COST (3)
value BCRYPT_MAX_COST (31)

+ fn bcrypt_hash(password: String, cost: uint (12)) String {
    // Generate a 16-byte random salt
    let salt_buf : [u64 x 2] = @undefined
    salt_buf[0] = u64.random()
    salt_buf[1] = u64.random()
    let salt = String.make_from_ptr(salt_buf, 16)
    // Generate hash
    let hash = utils:ByteBuffer.new(BCRYPT_HASH_STRING_LEN)
    bcrypt(cost, salt, password, hash)
    return hash
}

+ fn bcrypt_verify(password: String, hash: String) bool {
    let cost : uint = 0
  
    // Check the length of the hash string
    if hash.length != BCRYPT_HASH_STRING_LEN : return false
  
    // bcrypt uses the $2a$ prefix in the hash string
    if !hash.starts_with("$2a$") : return false
  
    // Parse cost parameter
    let cost_end = hash.index_of_byte('$', 4) ! return false
    cost = hash.part(4, cost_end - 4).to_uint() ! return false
  
    // Check the value of the cost parameter
    if cost < BCRYPT_MIN_COST || cost > BCRYPT_MAX_COST : return false
  
    // Parse salt parameter
    let hash_buf = utils:ByteBuffer.new(22)
    hash_buf.append_str(hash.part(cost_end + 1, 22))
    let salt_buf = utils:ByteBuffer.new(32)
    radix64_decode(hash_buf, salt_buf) ! return false
    let salt : String = salt_buf
  
    // Hash the password using bcrypt algorithm
    hash_buf.clear()
    hash_buf.minimum_free_space(BCRYPT_HASH_STRING_LEN)
    bcrypt(cost, salt, password, hash_buf)
  
    // The calculated string is bitwise compared to the hash string. The password is correct if and only if the strings match
    let i : uint = 0
    let mask : u8 = 0
    while i < BCRYPT_HASH_STRING_LEN {
       mask |= hash_buf[i] ^ hash[i]
       i++
    }
  
    return mask == 0
}
  
+ fn bcrypt(cost: uint, salt: String, password: String, output: utils:ByteBuffer) {

    let context = BlowfishContext {}
    let buf1 = utils:ByteBuffer.new(24)
    let buf2 = utils:ByteBuffer.new(24)
    buf1.append_str(salt)
    buf2.append_str(password)
    buf2.append_byte(0)
  
    eks_blowfish_setup(context, cost, buf1, buf2) _
  
    buf2.clear()
    buf2.append_str("OrpheanBeholderScryDoubt")
  
    // Repeatedly encrypt the text "OrpheanBeholderScryDoubt" 64 times
    let i : uint = 0
    let data = buf2.data.to_ptr()
    while i < 64 {
       // Perform encryption using Blowfish in ECB mode
       blowfish_encrypt_block(context, data, data)
       blowfish_encrypt_block(context, data + 8, data + 8)
       blowfish_encrypt_block(context, data + 16, data + 16)
       i++
    }

    // bcrypt uses the $2a$ prefix in the hash string
    let hash_start = "$2a$%{ cost.to_base(10).pad_left('0', 2) }$"
  
    // Concatenate the salt and the ciphertext
    let buf3 = utils:ByteBuffer.new(24)
    output.clear()
    output.append_str(hash_start)
    radix64_encode(buf1, buf3)
    buf2.length-- // Remove last character
    output.append(buf3)
    buf3.clear()
    radix64_encode(buf2, buf3)
    output.append(buf3)
}
  
// Expensive key setup
fn eks_blowfish_setup(context: BlowfishContext, cost: uint, salt: utils:ByteBuffer, password: utils:ByteBuffer) !invalid_salt !invalid_password {
    // Initialize Blowfish state
    blowfish_init_state(context)
    // Perform the first key expansion
    blowfish_expand_key(context, salt, password) ! match(E) {
        E.invalid_salt => throw invalid_salt
        E.invalid_key => throw invalid_password
    }

    // The cost parameter specifies a key expansion iteration count as a power of two
    let n : u32 = 1.to(u32) << cost.to(u32)
    // Iterate as many times as desired
    let i : u32 = 0
    while i < n {
        // Perform key expansion with password
        blowfish_expand_key(context, null, password) ! throw invalid_password
        // Perform key expansion with salt
        blowfish_expand_key(context, null, salt) ! throw invalid_salt
        i++
    }
}
