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))) |