bcrypt password hashing in Common Lisp

Native support for bcrypt password hashes was somewhat missing in the Common Lisp ecosystem, unless you count the various CFFI wrappers.

In recent releases of ironclad you can now find native Common Lisp implementations for bcrypt and bcrypt-pbkdf. Both of these have been implemented as part of this request.

Given the recently added support for bcrypt in ironclad I’ve packaged things up in a new Common Lisp system, which makes it easy to work with bcrypt password hashes in Common Lisp - cl-bcrypt.

With cl-bcrypt we can now generate, parse and verify bcrypt password hashes.

First, lets load the cl-bcrypt system.

CL-USER> (ql:quickload :cl-bcrypt)

The supported hash algorithm identifiers by cl-bcrypt are 2a and 2b.

In order to create a new bcrypt password you need to use the BCRYPT:MAKE-PASSWORD function, e.g.

CL-USER> (defparameter *password*
           (bcrypt:make-password "my-secret-password"))
*PASSWORD*

BCRYPT:MAKE-PASSWORD accepts keyword parameters, which allow you to specify a different salt (e.g. obtained by BCRYPT:GENERATE-SALT), different cost factor than the default, and a different algorithm identifier than the default (e.g. 2a).

If you don’t specify explicitely a salt, a random one will be generated for you by the BCRYPT:GENERATE-SALT function.

This example specifies a cost factor of 16 and a hash algorithm identifier 2a.

CL-USER> (defparameter *password*
           (bcrypt:make-password "my-secret-password" :cost 16 :identifier "2a"))
*PASSWORD*

You can use the BCRYPT:ALGORITHM-IDENTIFIER, BCRYPT:COST-FACTOR, BCRYPT:SALT and BCRYPT:PASSWORD-HASH readers to inspect the returned BCRYPT:PASSWORD instance from the BCRYPT:MAKE-PASSWORD function, e.g.

CL-USER> (bcrypt:algorithm-identifier *password*)
"2a"
CL-USER> (bcrypt:cost-factor *password*)
16
CL-USER> (bcrypt:salt *password*)
#(18 117 245 59 29 97 63 72 199 11 254 164 52 87 213 169)
CL-USER> (bcrypt:password-hash *password*)
#(94 0 171 116 90 235 30 220 57 45 147 214 210 77 244 223 63 14 153 13 140 213 183)

The BCRYPT:SALT and BCRYPT:PASSWORD-HASH readers return the raw bytes of the salt and the password hash respectively.

In order to encode a BCRYPT:PASSWORD instance into its text representation you need to use the BCRYPT:ENCODE function.

CL-USER> (bcrypt:encode *password*)
"$2a$16$ClVzMvzfNyhFA94iLDdToOVeApbDppFru3JXNUyi1y1x6MkO0KzZa"

A bcrypt password hash can be decoded using the BCRYPT:DECODE function, which will return a new instance of BCRYPT:PASSWORD, e.g.

CL-USER> (bcrypt:decode "$2a$16$ClVzMvzfNyhFA94iLDdToOVeApbDppFru3JXNUyi1y1x6MkO0KzZa")
#<CL-BCRYPT:PASSWORD {1002207AD3}>

If you encode back the returned instance you should get the same hash string as the one that was decoded.

The BCRYPT:PARSE-HASH function returns a property list of the parts that comprise the bcrypt hash string.

CL-USER> (bcrypt:parse-hash "$2a$16$ClVzMvzfNyhFA94iLDdToOVeApbDppFru3JXNUyi1y1x6MkO0KzZa")
(:ALGORITHM-IDENTIFIER "2a"
 :COST-FACTOR "16"
 :SALT "ClVzMvzfNyhFA94iLDdToO"
 :PASSWORD-HASH "VeApbDppFru3JXNUyi1y1x6MkO0KzZa")

When you need to test whether a given bcrypt hash matches a given password you can use the BCRYPT:PASSWORD= predicate, e.g.

CL-USER> (bcrypt:password= "my-secret-password"
                           "$2a$16$ClVzMvzfNyhFA94iLDdToOVeApbDppFru3JXNUyi1y1x6MkO0KzZa")
T

For more information, please refer to the cl-bcrypt repo.

Written on September 11, 2020