Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
encryption.clj 3.96 KiB
(ns metabase.util.encryption
  "Utility functions for encrypting and decrypting strings using AES256 CBC + HMAC SHA512 and the
  `MB_ENCRYPTION_SECRET_KEY` env var."
  (:require [buddy.core
             [codecs :as codecs]
             [crypto :as crypto]
             [kdf :as kdf]
             [nonce :as nonce]]
            [clojure.tools.logging :as log]
            [environ.core :as env]
            [metabase.util :as u]
            [puppetlabs.i18n.core :refer [trs]]
            [ring.util.codec :as codec]))

(defn secret-key->hash
  "Generate a 64-byte byte array hash of `secret-key` using 100,000 iterations of PBKDF2+SHA512."
  ^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))

;; apperently if you're not tagging in an arglist, `^bytes` will set the `:tag` metadata to `clojure.core/bytes` (ick)
;; so you have to do `^{:tag 'bytes}` instead
(defonce ^:private ^{:tag 'bytes} default-secret-key
  (when-let [secret-key (env/env :mb-encryption-secret-key)]
    (when (seq secret-key)
      (assert (>= (count secret-key) 16)
        (trs "MB_ENCRYPTION_SECRET_KEY must be at least 16 characters."))
      (secret-key->hash secret-key))))

;; log a nice message letting people know whether DB details encryption is enabled
(log/info
 (if default-secret-key
   (trs "Saved credentials encryption is ENABLED for this Metabase instance.")
   (trs "Saved credentials encryption is DISABLED for this Metabase instance."))
 (u/emoji (if default-secret-key "🔐" "🔓"))
 (trs "\nFor more information, see")
 "https://www.metabase.com/docs/latest/operations-guide/start.html#encrypting-your-database-connection-details-at-rest")

(defn encrypt
  "Encrypt string `s` as hex bytes using a `secret-key` (a 64-byte byte array), by default the hashed value of
  `MB_ENCRYPTION_SECRET_KEY`."
  (^String [^String s]
   (encrypt default-secret-key s))
  (^String [^String secret-key, ^String s]
   (let [initialization-vector (nonce/random-bytes 16)]
     (codec/base64-encode
      (byte-array
       (concat initialization-vector
               (crypto/encrypt (codecs/to-bytes s) secret-key initialization-vector
                               {:algorithm :aes256-cbc-hmac-sha512})))))))

(defn decrypt
  "Decrypt string `s` using a `secret-key` (a 64-byte byte array), by default the hashed value of
  `MB_ENCRYPTION_SECRET_KEY`."
  (^String [^String s]
   (decrypt default-secret-key s))
  (^String [secret-key, ^String s]
   (let [bytes                           (codec/base64-decode s)
         [initialization-vector message] (split-at 16 bytes)]
     (codecs/bytes->str (crypto/decrypt (byte-array message) secret-key (byte-array initialization-vector)
                                        {:algorithm :aes256-cbc-hmac-sha512})))))


(defn maybe-encrypt
  "If `MB_ENCRYPTION_SECRET_KEY` is set, return an encrypted version of `s`; otherwise return `s` as-is."
  (^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)))

(defn maybe-decrypt
  "If `MB_ENCRYPTION_SECRET_KEY` is set and `s` is encrypted, decrypt `s`; otherwise return `s` as-is."
  (^String [^String s]
   (maybe-decrypt default-secret-key s))
  (^String [secret-key, ^String s]
   (if (and secret-key (seq s))
     (try
       (decrypt secret-key s)
       (catch Throwable e
         (if (u/base64-string? s)
           ;; if we can't decrypt `s`, but it *is* encrypted, log an error message and return `nil`
           (log/error
            (trs "Cannot decrypt encrypted credentials. Have you changed or forgot to set MB_ENCRYPTION_SECRET_KEY?")
            (.getMessage e)
            (u/filtered-stacktrace e))
           ;; otherwise return S without decrypting. It's probably not decrypted in the first place
           s)))
     s)))