Permalink
Browse files

Add Scrypt key derivation function.

  • Loading branch information...
1 parent 8773097 commit 61afbeb39f9c0598695a9618b8a942335ac7700c @howeyc committed Sep 8, 2012
Showing with 150 additions and 8 deletions.
  1. +21 −6 doc/ironclad-doc.txt
  2. +2 −0 ironclad.asd
  3. +9 −0 src/conditions.lisp
  4. +3 −2 src/package.lisp
  5. +92 −0 src/scrypt.lisp
  6. +23 −0 testing/test-vectors/scrypt.lisp
View
@@ -425,19 +425,23 @@ passed to " @make-hmac " when " 'hmac' " was constructed.")
@insufficient-buffer-space " error will be signaled if there is
insufficient space in " 'buffer' ".")
-(:h3 "PBKDF")
-
-(:p "Ironclad comes with the basic PBKDF1 and PBKDF2 algorithms, as
-well as convenience functions for using them to store passwords.")
+((:h2 id "kdfs") "Key derivation functions")
(:describe :function (ironclad:derive-key digest))
(:p "Given a key derivation function object (produced by " `make-kdf`
-"), a password and salt (both must be of type " `(SIMPLE-ARRAY
+ or `make-scrypt-kdf`"), a password and salt (both must be of type " `(SIMPLE-ARRAY
(UNSIGNED-BYTE 8) (*))` "), and number of digest iterations, returns
the password digest as a byte array of length " 'key-length' ".")
-(:describe :function (ironclad:make-kdf kind))
+(:p "Scrypt ignores " 'iteration-count' "parameter.")
+
+(:h3 "PBKDF")
+
+(:p "Ironclad comes with the basic PBKDF1 and PBKDF2 algorithms, as
+well as convenience functions for using them to store passwords.")
+
+(:describe :function (ironclad:make-kdf kdf))
(:p "Returns a key derivation function instance (" 'kind' " must
either be " 'pbkdf1' " or " 'pbkdf2' ") that uses the given "
@@ -461,6 +465,17 @@ that encodes the given salt and PBKDF2 algorithm parameters.")
produced by " `pbkdf2-hash-password-to-combined-string` ", checks whether
the password is valid.")
+(:h3 "Scrypt")
+
+(:p "Ironclad comes with and implementation of the scrypt key
+derivation function as defined by Colin Percival's " (:url
+"http://www.tarsnap.com/scrypt.html"
+"The scrypt key derivation function."))
+
+(:describe :function (ironclad:make-scrypt-kdf kdf))
+
+(:p "Returns an scrypt key derivation function instance.")
+
(:h3 "CMACs")
(:p "Instances of CMACs are constructed by specifying a secret key and a
View
@@ -49,6 +49,7 @@
(:file "octet-stream" :depends-on ("common"))
(:file "padding" :depends-on ("common"))
(:file "pkcs5" :depends-on ("common"))
+ (:file "scrypt" :depends-on ("pkcs5"))
(:file "password-hash" :depends-on ("pkcs5"))
(:file "math" :depends-on ("prng" "public-key"))
(:module "sbcl-opt"
@@ -226,6 +227,7 @@
(:file "digests")
(:file "padding")
(:file "pkcs5")
+ (:file "scrypt")
(:file "ironclad")
(:file "prng")
;; test vectors
View
@@ -64,6 +64,15 @@ for a particular mode of operation but not supplied."))
(digest condition))))
(:documentation "Signaled when an invalid digest name is provided to MAKE-DIGEST."))
+(define-condition unsupported-scrypt-cost-factors (ironclad-error)
+ ((N :initarg :N :reader cost-N)
+ (r :initarg :r :reader cost-r)
+ (p :initarg :p :reader cost-p))
+ (:report (lambda (condition stream)
+ (format stream "Scrypt cost factors not supported. N=~A must be a power of two and (r=~A * p=~A) <= 2 power of 30."
@froydnj

froydnj Sep 11, 2012

Just say "2^30" instead of "2 power of 30".

+ (cost-N condition) (cost-r condition) (cost-p condition))))
+ (:documentation "Signaled when a invalid cost factors are provided to MAKE-SCRYPT-KDF."))
+
(define-condition insufficient-buffer-space (ironclad-error)
((buffer :initarg :buffer :reader insufficient-buffer-space-buffer)
(start :initarg :start :reader insufficient-buffer-space-start)
View
@@ -33,8 +33,8 @@
#:ecb #:cbc #:ctr #:ofb #:cfb #:stream
;; KDFs
- #:pbkdf1 #:pbkdf2
- #:make-kdf #:derive-key
+ #:pbkdf1 #:pbkdf2 #:scryptkdf
+ #:make-kdf #:make-scrypt-kdf #:derive-key
;; KDF convenience functions
#:make-random-salt #:pbkdf2-hash-password
@@ -65,6 +65,7 @@
#:ironclad-error #:initialization-vector-not-supplied
#:invalid-initialization-vector #:invalid-key-length
#:unsupported-cipher #:unsupported-mode #:unsupported-digest
+ #:unsupported-scrypt-cost-factors
#:insufficient-buffer-space #:invalid-padding
#:key-not-supplied
View
@@ -0,0 +1,92 @@
+;;;; -*- mode: lisp; indent-tabs-mode: nil -*-
+(in-package :crypto)
+
+
+;;; scrypt from Colin Percival's
+;;; "Stronger Key Derivation via Sequential Memory-Hard Functions"
+;;; presented at BSDCan'09, May 2009.
+;;; http://www.tarsnap.com/scrypt.html
+
+(defclass scryptkdf ()
+ ((N :accessor scrypt-kdf-N
+ :initarg :N
+ :initform 16384)
@froydnj

froydnj Sep 11, 2012

Minor style nits on this class:

  • I think these :INITFORMs are unnecessary, given that you always pass the appropriate :INITARGs;
  • The :ACCESSORs should be :READERs, given that you never set these;
  • I think the class reads slightly better as SCRYPT-KDF, but I'm not going to complain if you'd rather keep it the way it is.
+ (r :accessor scrypt-kdf-r
+ :initarg :r
+ :initform 8)
+ (p :accessor scrypt-kdf-p
+ :initarg :p
+ :initform 1)))
+
+(defmacro salsa-vector-4mix (x i4 i8 i12 i0)
+ `(setf (aref ,x ,i4) (ldb (byte 32 0) (logxor (aref ,x ,i4) (rol32 (mod32+ (aref ,x ,i0) (aref ,x ,i12)) 7)))
+ (aref ,x ,i8) (ldb (byte 32 0) (logxor (aref ,x ,i8) (rol32 (mod32+ (aref ,x ,i4) (aref ,x ,i0)) 9)))
+ (aref ,x ,i12) (ldb (byte 32 0) (logxor (aref ,x ,i12) (rol32 (mod32+ (aref ,x ,i8) (aref ,x ,i4)) 13)))
+ (aref ,x ,i0) (ldb (byte 32 0) (logxor (aref ,x ,i0) (rol32 (mod32+ (aref ,x ,i12) (aref ,x ,i8)) 18)))))
+
+(defun scrypt-vector-salsa (b)
+ (let ((x (make-array 16 :element-type '(unsigned-byte 32)))
+ (w (make-array 16 :element-type '(unsigned-byte 32))))
@froydnj

froydnj Sep 11, 2012

Please declare these two arrays DYNAMIC-EXTENT.

+ (declare (type (simple-array (unsigned-byte 32) (16)) x w))
+ (fill-block-ub8-le x b 0)
+ (replace w x)
+
+ (loop repeat 4 do
+ (salsa-vector-4mix x 4 8 12 0)
+ (salsa-vector-4mix x 9 13 1 5)
+ (salsa-vector-4mix x 14 2 6 10)
+ (salsa-vector-4mix x 3 7 11 15)
+ (salsa-vector-4mix x 1 2 3 0)
+ (salsa-vector-4mix x 6 7 4 5)
+ (salsa-vector-4mix x 11 8 9 10)
+ (salsa-vector-4mix x 12 13 14 15))
+
+ (dotimes (i 16)
+ (setf (nibbles:ub32ref/le b (* i 4)) (mod32+ (aref x i) (aref w i))))))
+
+(defun block-mix (b xy xy-start r)
+ (let ((xs (make-array 64 :element-type '(unsigned-byte 8))))
@froydnj

froydnj Sep 11, 2012

Please make this DYNAMIC-EXTENT and declare its type.

+ (replace xs b :start2 (* 64 (1- (* 2 r))) :end1 64)
+ (dotimes (i (* 2 r))
+ (xor-block 64 xs b (* i 64) xs 0)
+ (scrypt-vector-salsa xs)
+ (replace xy xs :start1 (+ xy-start (* i 64)) :end2 64))
+ (dotimes (i r)
+ (replace b xy :start1 (* i 64) :end1 (+ 64 (* i 64)) :start2 (+ xy-start (* 64 2 i))))
+ (dotimes (i r)
+ (replace b xy :start1 (* 64 (+ i r)) :end1 (+ (* 64 (+ i r)) 64) :start2 (+ xy-start (* 64 (1+ (* i 2))))))))
+
+(defun smix (b b-start r N v xy)
+ (let ((x xy)
+ (xy-start (* 128 r)))
+ (replace x b :end1 (* 128 r) :start2 b-start)
@froydnj

froydnj Sep 11, 2012

Can we use XY-START everywhere (* 128 R) appears in this function? CL compilers are generally not that sophisticated about optimizing repeated expressions (as opposed to C compilers, say). Doing the same sort of thing in BLOCK-MIX would be helpful, too.

+ (dotimes (i N)
+ (replace v x :start1 (* i 128 r) :end2 (* 128 r))
+ (block-mix x xy xy-start r))
+ (dotimes (i N)
+ (let ((j (ldb (byte 32 0) (logand (nibbles:ub64ref/le x (* (1- (* 2 r)) 64)) (1- N)))))
+ (xor-block (* 128 r) x v (* j 128 r) x 0)
+ (block-mix x xy xy-start r)))
+ (replace b x :start1 b-start :end1 (* 128 r))))
+
+(defmethod derive-key ((kdf scryptkdf) passphrase salt iteration-count key-length)
+ (declare (ignore iteration-count))
+ (let ((xy (make-array (* 256 (scrypt-kdf-r kdf)) :element-type '(unsigned-byte 8)))
+ (v (make-array (* 128 (scrypt-kdf-r kdf) (scrypt-kdf-N kdf)) :element-type '(unsigned-byte 8)))
+ (b (derive-key (make-kdf 'PBKDF2 :digest 'SHA256) passphrase salt 1 (* (scrypt-kdf-p kdf) 128 (scrypt-kdf-r kdf)))))
@froydnj

froydnj Sep 11, 2012

Minor efficiency point: you could create the pbkdf2 instance once, use it here, and then REINITIALIZE-INSTANCE on it before using it below.

+ (dotimes (i (scrypt-kdf-p kdf))
+ (smix b (* i 128 (scrypt-kdf-r kdf)) (scrypt-kdf-r kdf) (scrypt-kdf-N kdf) v xy))
+ (derive-key (make-kdf 'PBKDF2 :digest 'SHA256) passphrase b 1 key-length)))
+
+(defun make-scrypt-kdf (&optional (N 16384 N-supplied-p) (r 8 r-supplied-p) (p 1 p-supplied-p))
@froydnj

froydnj Sep 11, 2012

A couple of comments about this function. I think it would be acceptable to add N, r, and p keyword args to MAKE-KDF, even if those keywords aren't going to be used for PBKDF-esque KDFs. There should be a little bit of documentation describing the applicability of particular keyword arguments.

Also, I think the -supplied-p variables are unnecessary; removing them also means that you sanity check your default arguments, which is a good thing.

+ "N is a CPU/memory cost parameter, and must be a power of two greater than 1.
+ r and p must satisfy (< (* r p) (expt 2 30)). If the parameters do not satisfy
+ the limits, that results in an unsupported-scrypt-costs error condition.
+
+ The recommended paramters for interactive logins as of 2009 are:
+ N=16384, r=8, p=1. They should be increased as memory latency and CPU parallelism
+ increases."
+ (when (or (and N-supplied-p (or (<= N 1) (not (zerop (logand N (1- N))))))
+ (and (or r-supplied-p p-supplied-p) (>= (* r p) (expt 2 30))))
+ (error 'unsupported-scrypt-cost-factors :N N :r r :p p))
+ (make-instance 'scryptkdf :N N :r r :p p))
@@ -0,0 +1,23 @@
+;;;; -*- mode: lisp; indent-tabs-mode: nil -*-
+(in-package :crypto-tests)
+
+;;; Test vectors based on those defined in the Go implementation.
+;;;
+;;; http://github.com/dchest/scrypt
+
+(defvar *scrypt-password*
+ (coerce #(112 97 115 115 119 111 114 100)
+ '(vector (unsigned-byte 8))))
+(defvar *scrypt-salt*
+ (coerce #(115 97 108 116)
+ '(vector (unsigned-byte 8))))
+
+(defvar *scrypt-key*
+ (coerce #(116 87 49 175 68 132 243 35 150 137 105 237 162 137 174 238 0 91 89 3 172 86
+ 30 100 165 172 161 33 121 123 247 115)
+ '(vector (unsigned-byte 8))))
+
+(rtest:deftest scryptkdf
+ (run-kdf-test (crypto:make-scrypt-kdf)
+ *scrypt-password* *scrypt-salt* 1000 (length *scrypt-key*) *scrypt-key*)
+ t)

1 comment on commit 61afbeb

Thanks for updating the documentation, too! Don't worry about regenerating the HTML; I'll do that separately.

Please sign in to comment.