Utility functions for encrypting and decrypting strings using AES256 CBC + HMAC SHA512 and the
| (ns metabase.util.encryption (:require [buddy.core.bytes :as bytes] [buddy.core.codecs :as codecs] [buddy.core.crypto :as crypto] [buddy.core.kdf :as kdf] [buddy.core.nonce :as nonce] [clojure.string :as str] [environ.core :as env] [metabase.util :as u] [metabase.util.i18n :refer [trs]] [metabase.util.log :as log] [ring.util.codec :as codec]) (:import (java.io ByteArrayInputStream InputStream SequenceInputStream) (javax.crypto Cipher CipherInputStream) (javax.crypto.spec SecretKeySpec IvParameterSpec))) |
(set! *warn-on-reflection* true) | |
(def ^:private ^:const aes-streaming-spec "AES/CBC/PKCS5Padding") | |
Generate a 64-byte byte array hash of | (defn secret-key->hash ^bytes [^String secret-key] (kdf/get-bytes (kdf/engine {:alg :pbkdf2+sha512 :key secret-key :iterations 100000}) ; 100,000 iterations takes about ~160ms on my laptop 64)) |
Check the minimum length of the key and hash it for internal usage. | (defn validate-and-hash-secret-key [^String secret-key] (when-let [secret-key secret-key] (when (seq secret-key) (assert (>= (count secret-key) 16) (str (trs "MB_ENCRYPTION_SECRET_KEY must be at least 16 characters."))) (secret-key->hash secret-key)))) |
apperently if you're not tagging in an arglist, | (defonce ^:private ^{:tag 'bytes} default-secret-key (validate-and-hash-secret-key (env/env :mb-encryption-secret-key))) |
Is the | (defn default-encryption-enabled? [] (boolean default-secret-key)) |
log a nice message letting people know whether DB details encryption is enabled | (when-not *compile-files* (log/info (if default-secret-key "Saved credentials encryption is ENABLED for this Metabase instance." "Saved credentials encryption is DISABLED for this Metabase instance.") (u/emoji (if default-secret-key "🔐" "🔓")) "\n" "For more information, see https://metabase.com/docs/latest/operations-guide/encrypting-database-details-at-rest.html")) |
Encrypt bytes | (defn encrypt-bytes {:added "0.41.0"} (^String [^bytes b] (encrypt-bytes default-secret-key b)) (^String [^String secret-key, ^bytes b] (let [initialization-vector (nonce/random-bytes 16)] (->> (crypto/encrypt b secret-key initialization-vector {:algorithm :aes256-cbc-hmac-sha512}) (concat initialization-vector) byte-array)))) |
Encrypt string | (defn encrypt (^String [^String s] (encrypt default-secret-key s)) (^String [^String secret-key, ^String s] (->> (codecs/to-bytes s) (encrypt-bytes secret-key) codec/base64-encode))) |
Decrypt bytes | (defn decrypt-bytes {:added "0.41.0"} (^String [^bytes b] (decrypt-bytes default-secret-key b)) (^String [secret-key, ^bytes b] (let [[initialization-vector message] (split-at 16 b)] (crypto/decrypt (byte-array message) secret-key (byte-array initialization-vector) {:algorithm :aes256-cbc-hmac-sha512})))) |
Wraps a plaintext input stream into an input stream that encrypts it using AES256 CBC. The encryption format is slightly different for streams vs. fixed length data | (defn encrypt-stream {:added "0.53.0"} (^InputStream [^InputStream input-stream] (encrypt-stream default-secret-key input-stream)) (^InputStream [secret-key ^InputStream input-stream] (let [spec aes-streaming-spec spec-header (codecs/to-bytes (format "%-32s" spec)) cipher (Cipher/getInstance spec) iv (nonce/random-bytes 16)] (.init cipher Cipher/ENCRYPT_MODE (SecretKeySpec. (bytes/slice secret-key 32 64) "AES") (IvParameterSpec. iv)) (SequenceInputStream. (ByteArrayInputStream. (bytes/concat spec-header iv)) (CipherInputStream. input-stream cipher))))) |
Encrypts a byte-array in a way that can be used to read it with decrypt-stream instead of decrypt. | (defn encrypt-for-stream {:added "0.53.0"} (^bytes [^bytes input] (encrypt-for-stream default-secret-key input)) (^bytes [secret-key ^bytes input] (with-open [encrypted (encrypt-stream secret-key (ByteArrayInputStream. input))] (.readAllBytes encrypted)))) |
Wraps a possibly-encrypted input stream into a new input stream that decrypts it if necessary. | (defn maybe-decrypt-stream {:added "0.53.0"} (^InputStream [^InputStream input-stream] (maybe-decrypt-stream default-secret-key input-stream)) (^InputStream [secret-key ^InputStream input-stream] (let [spec-array (byte-array 32) spec-array-length (.read input-stream spec-array) spec (str/trim (codecs/bytes->str spec-array))] (cond (= spec-array-length -1) input-stream (and (= spec-array-length 32) (= spec aes-streaming-spec)) (let [cipher (Cipher/getInstance spec) iv (byte-array 16) _ (.read input-stream iv)] (.init cipher Cipher/DECRYPT_MODE (SecretKeySpec. (bytes/slice secret-key 32 64) "AES") (IvParameterSpec. iv)) (CipherInputStream. input-stream cipher)) :else (SequenceInputStream. (ByteArrayInputStream. (bytes/slice spec-array 0 spec-array-length)) input-stream))))) |
Decrypt string | (defn decrypt (^String [^String s] (decrypt default-secret-key s)) (^String [secret-key, ^String s] (codecs/bytes->str (decrypt-bytes secret-key (codec/base64-decode s))))) |
If | (defn maybe-encrypt (^String [^String s] (maybe-encrypt default-secret-key s)) (^String [secret-key, ^String s] (if secret-key (when (seq s) (encrypt secret-key s)) s))) |
If | (defn maybe-encrypt-bytes {:added "0.41.0"} (^bytes [^bytes b] (maybe-encrypt-bytes default-secret-key b)) (^bytes [secret-key, ^bytes b] (if secret-key (when (seq b) (encrypt-bytes secret-key b)) b))) |
If | (defn maybe-encrypt-for-stream (^bytes [^bytes s] (maybe-encrypt-for-stream default-secret-key s)) (^bytes [secret-key, ^bytes s] (if secret-key (encrypt-for-stream secret-key s) s))) |
(def ^:private ^:const aes256-tag-length 32) (def ^:private ^:const aes256-block-size 16) | |
Returns true if it's likely that | (defn possibly-encrypted-bytes? [^bytes b] (if (nil? b) false (u/ignore-exceptions (when-let [byte-length (alength b)] (zero? (mod (- byte-length aes256-tag-length) aes256-block-size)))))) |
Returns true if it's likely that | (defn possibly-encrypted-string? [^String s] (u/ignore-exceptions (when-let [b (and (not (str/blank? s)) (u/base64-string? s) (codec/base64-decode s))] (possibly-encrypted-bytes? b)))) |
If | (defn maybe-decrypt {:arglists '([secret-key? s])} [& args] ;; secret-key as an argument so that tests can pass it directly without using `with-redefs` to run in parallel (let [[secret-key v] (if (and (bytes? (first args)) (string? (second args))) args (cons default-secret-key args)) log-error-fn (fn [kind ^Throwable e] (log/warnf e "Cannot decrypt encrypted %s. Have you changed or forgot to set MB_ENCRYPTION_SECRET_KEY?" kind))] (cond (nil? secret-key) v (possibly-encrypted-string? v) (try (decrypt secret-key v) (catch Throwable e ;; if we can't decrypt `v`, but it *is* probably encrypted, log a warning (log-error-fn "String" e) v)) (possibly-encrypted-bytes? v) (try (decrypt-bytes secret-key v) (catch Throwable e ;; if we can't decrypt `v`, but it *is* probably encrypted, log a warning (log-error-fn "bytes" e) v)) :else v))) |