diff --git a/project.clj b/project.clj index e7569ec..12f90ca 100644 --- a/project.clj +++ b/project.clj @@ -6,10 +6,9 @@ :min-lein-version "2.0.0" :dependencies [[org.clojure/clojure "1.6.0"] [ring/ring-core "1.2.0"] + [crypto-password "0.1.3"] [slingshot "0.10.2"] - [org.mindrot/jbcrypt "0.3m"] - ;; http-basic [commons-codec "1.6"] diff --git a/src/cemerick/friend/credentials.clj b/src/cemerick/friend/credentials.clj index 866ec7d..51f6487 100644 --- a/src/cemerick/friend/credentials.clj +++ b/src/cemerick/friend/credentials.clj @@ -1,5 +1,19 @@ (ns cemerick.friend.credentials - (:import org.mindrot.jbcrypt.BCrypt)) + (:require [crypto.password.bcrypt :as bcrypt] + [crypto.password.pbkdf2 :as pbkdf2] + [crypto.password.scrypt :as scrypt])) + +(defn- build-credential-fn + "Builds a credential function from the given verify function. The + verify function must accept two arguments and verifies that the + plaintext password in the first argument hashes to the second + argument" + [verify-fn] + (fn [load-credentials-fn {:keys [username password]}] + (when-let [creds (load-credentials-fn username)] + (let [password-key (or (-> creds meta ::password-key) :password)] + (when (verify-fn password (get creds password-key)) + (dissoc creds password-key)))))) (defn hash-bcrypt "Hashes a given plaintext password using bcrypt and an optional @@ -7,17 +21,17 @@ passwords included in stored user credentials that are to be later verified using `bcrypt-credential-fn`." [password & {:keys [work-factor]}] - (BCrypt/hashpw password (if work-factor - (BCrypt/gensalt work-factor) - (BCrypt/gensalt)))) + (if work-factor + (bcrypt/encrypt password work-factor) + (bcrypt/encrypt password))) (defn bcrypt-verify "Returns true if the plaintext [password] corresponds to [hash], -the result of previously hashing that password." + the result of previously hashing that password with bcrypt." [password hash] - (BCrypt/checkpw password hash)) + (bcrypt/check password hash)) -(defn bcrypt-credential-fn +(def bcrypt-credential-fn "A bcrypt credentials function intended to be used with `cemerick.friend/authenticate` or individual authentication workflows. You must supply a function of one argument that will look up stored user credentials given a username/id. e.g.: @@ -41,8 +55,100 @@ the result of previously hashing that password." ...then the hash will be verified correctly as long as the credentials map contains a [:cemerick.friend.credentials/password-key :app.foo/passphrase] entry." - [load-credentials-fn {:keys [username password]}] - (when-let [creds (load-credentials-fn username)] - (let [password-key (or (-> creds meta ::password-key) :password)] - (when (bcrypt-verify password (get creds password-key)) - (dissoc creds password-key))))) \ No newline at end of file + (build-credential-fn bcrypt-verify)) + +(defn hash-pbkdf2 + "Hashes the given plaintext password using pbkdf2. An optional + number of :iterations can be specified (defaults to 100,000) and, if + the number of iterations is specified, an optional :salt can also + be specified (defaults to 8 random bytes). Should be used to hash + passwords included in stored user credentials that are to be later + verified using `pbkdf2-credential-fn`." + [password & {:keys [iterations salt]}] + (if iterations + (if salt + (pbkdf2/encrypt password iterations salt) + (pbkdf2/encrypt password iterations)) + (pbkdf2/encrypt password))) + +(defn pbkdf2-verify + "Returns true if the plantext [password] corresponds to [hash], the + result of previously hashing that password with pbkdf2." + [password hash] + (pbkdf2/check password hash)) + +(def pbkdf2-credential-fn + "A pbkdf2 credentials function intended to be used with + `cemerick.friend/authenticate` or individual authentication + workflows. You must supply a function of one argument that will look + up stored user credentials given a username/id. e.g.: + + (authenticate {:credential-fn (partial pbkdf2-credential-fn load-user-record) + :other :config ...} + ring-handler-to-be-secured) + + If the provided (cleartext, user-supplied) password matches the + hashed password in the credentials map, that map is returned. + + The password in the credentials map will be looked up using + a :password key by default; if the credentials-loading function will + return a credentials map that stores the hashed password under a + different key, it must specify that alternative key via + a :cemerick.friend.credentials/password-key slot in the map's + metadata. So, if a credentials map looks like: + + {:username \"joe\" :app.foo/passphrase \"pbkdf2 hash\"} + + ...then the hash will be verified correctly as long as the + credentials map contains + a [:cemerick.friend.credentials/password-key :app.foo/passphrase] + entry." + (build-credential-fn pbkdf2-verify)) + +(defn hash-scrypt + "Hashes the given plaintext password using scrypt. an optional :cpu + cost parameter (defaults to 2^15) can be specified, but it must be a + power of 2. If the :cpu parameter is specified, then + optional :para (defaults to 1) and :ram (defaults to 8) parameters + can be specified for the memory cost and parallelization factor + respectively" + [password & {:keys [cpu ram para]}] + (if cpu + (if (and ram para) + (scrypt/encrypt password cpu ram para) + (scrypt/encrypt password cpu)) + (scrypt/encrypt password))) + +(defn scrypt-verify + "Returns true if the plaintext [password] corresponds to [hash], the + result of previously hashing that password with scrypt." + [password hash] + (scrypt/check password hash)) + +(def scrypt-credential-fn + "A scrypt credentials function intended to be used with + `cemerick.friend/authenticate` or individual authentication + workflows. You must supply a function of one argument that will look + up stored user credentials given a username/id. e.g.: + + (authenticate {:credential-fn (partial scrypt-credential-fn load-user-record) + :other :config ...} + ring-handler-to-be-secured) + + If the provided (cleartext, user-supplied) password matches the + hashed password in the credentials map, that map is returned. + + The password in the credentials map will be looked up using + a :password key by default; if the credentials-loading function will + return a credentials map that stores the hashed password under a + different key, it must specify that alternative key via + a :cemerick.friend.credentials/password-key slot in the map's + metadata. So, if a credentials map looks like: + + {:username \"joe\" :app.foo/passphrase \"scrypt hash\"} + + ...then the hash will be verified correctly as long as the + credentials map contains + a [:cemerick.friend.credentials/password-key :app.foo/passphrase] + entry." + (build-credential-fn scrypt-verify)) diff --git a/test/test_friend/credentials.clj b/test/test_friend/credentials.clj index 9ad4f32..9901b3f 100644 --- a/test/test_friend/credentials.clj +++ b/test/test_friend/credentials.clj @@ -2,20 +2,32 @@ (:require [cemerick.friend.credentials :as creds]) (:use clojure.test)) -(deftest simple +(deftest simple-bcrypt (is (= {:username "joe"} (creds/bcrypt-credential-fn {"username" {:username "joe" :password (creds/hash-bcrypt "foo")}} {:username "username" :password "foo"})))) -(deftest custom-password-key +(deftest custom-password-key-bcrypt (is (thrown? NullPointerException (creds/bcrypt-credential-fn {"username" {"username" "joe" ::password (creds/hash-bcrypt "foo")}} {:username "username" :password "foo"}))) - + (is (= {:username "joe"} (creds/bcrypt-credential-fn {"username" ^{::creds/password-key ::password} {:username "joe" ::password (creds/hash-bcrypt "foo")}} {:username "username" :password "foo"})))) + +(deftest simple-pbkdf2 + (is (= {:username "joe"} + (creds/pbkdf2-credential-fn + {"username" {:username "joe" :password (creds/hash-pbkdf2 "foo")}} + {:username "username" :password "foo"})))) + +(deftest simple-scrypt + (is (= {:username "joe"} + (creds/scrypt-credential-fn + {"username" {:username "joe" :password (creds/hash-scrypt "foo")}} + {:username "username" :password "foo"}))))