Utility functions for checking passwords against hashes and for making sure passwords match complexity requirements. | (ns metabase.util.password (:require [clojure.java.io :as io] [metabase.config :as config] [metabase.util :as u]) (:import (org.mindrot.jbcrypt BCrypt))) |
(set! *warn-on-reflection* true) | |
Return a map of the counts of each class of character for (count-occurrences "GoodPw!!") -> {:total 8, :lower 4, :upper 2, :letter 6, :digit 0, :special 2} | (defn- count-occurrences [password] (loop [[^Character c & more] password, counts {:total 0, :lower 0, :upper 0, :letter 0, :digit 0, :special 0}] (if-not c counts (recur more (let [counts (update counts :total inc)] (cond (Character/isLowerCase c) (-> (update counts :letter inc) (update :lower inc)) (Character/isUpperCase c) (-> (update counts :letter inc) (update :upper inc)) (Character/isDigit c) (update counts :digit inc) :else (update counts :special inc))))))) |
Minimum counts of each class of character a password should have for a given password complexity level. | (def ^:private ^:const complexity->char-type->min {:weak {:total 6} ; total here effectively means the same thing as a minimum password length :normal {:total 6 :digit 1} :strong {:total 8 :lower 2 :upper 2 :digit 1 :special 1}}) |
Check that PASSWORD satisfies the minimum count requirements for each character class. (password-has-char-counts? {:total 6, :lower 1, :upper 1, :digit 1, :special 1} "abc") -> false (password-has-char-counts? {:total 6, :lower 1, :upper 1, :digit 1, :special 1} "passworD1!") -> true | (defn- password-has-char-counts? [char-type->min password] {:pre [(map? char-type->min) (string? password)]} (let [occurences (count-occurrences password)] (boolean (loop [[[char-type min-count] & more] (seq char-type->min)] (if-not char-type true (when (>= (occurences char-type) min-count) (recur more))))))) |
The currently configured description of the password complexity rules being enforced | (defn active-password-complexity [] (merge (complexity->char-type->min (config/config-kw :mb-password-complexity)) ;; Setting MB_PASSWORD_LENGTH overrides the default :total for a given password complexity class (when-let [min-len (config/config-int :mb-password-length)] {:total min-len}))) |
Check if a given password meets complexity standards for the application. | (defn- is-complex? [password] (password-has-char-counts? (active-password-complexity) password)) |
A set of ~12k common passwords to reject, that otherwise meet Metabase's default complexity requirements. Sourced from Dropbox's zxcvbn repo: https://github.com/dropbox/zxcvbn/blob/master/data/passwords.txt | (def ^java.net.URL common-passwords-url (io/resource "common_passwords.txt")) |
Check if a given password is not present in the common passwords set. Case-insensitive search since the list only contains lower-case passwords. | (defn- is-uncommon? [password] (with-open [is (.openStream common-passwords-url) reader (java.io.BufferedReader. (java.io.InputStreamReader. is))] (not-any? (partial = (u/lower-case-en password)) (iterator-seq (.. reader lines iterator))))) |
Check that a password both meets complexity standards, and is not present in the common passwords list. Common password list is ignored if minimum password complexity is set to :weak | (defn is-valid? [password] (and (is-complex? password) (or (= (config/config-kw :mb-password-complexity) :weak) (is-uncommon? password)))) |
Default work factor used for hashing passwords with BCrypt. Intentionally minimal for tests to reduce testing time. | (def ^:private default-bcrypt-work-factor (if config/is-test? ;; 4 is the minimum supported value by jbcrypt library. 4 10)) |
Hashes a given plaintext password using bcrypt and an optional
:work-factor (defaults to 10 as of this writing). Should be used to hash
passwords included in stored user credentials that are to be later verified
using copied from cemerick.friend.credentials EPL v1.0 license | (defn hash-bcrypt [password & {:keys [work-factor]}] (BCrypt/hashpw password (if work-factor (BCrypt/gensalt work-factor) (BCrypt/gensalt default-bcrypt-work-factor)))) |
Returns true if the plaintext [password] corresponds to [hash], the result of previously hashing that password. | (defn bcrypt-verify [password hash] (BCrypt/checkpw password hash)) |
Verify if a given unhashed password + salt matches the supplied hashed-password. Returns | (defn verify-password ^Boolean [password salt hashed-password] ;; we wrap the friend/bcrypt-verify with this function specifically to avoid unintended exceptions getting out (boolean (u/ignore-exceptions (bcrypt-verify (str salt password) hashed-password)))) |