|
(ns client-encrypt |
|
"Utilities to for encrypting credentials (chiefly API keys), |
|
storing them on disk, and editing them." |
|
(:require [buddy.core.codecs :as codecs] |
|
[buddy.core.nonce :as nonce] |
|
[buddy.core.crypto :as crypto] |
|
[buddy.core.kdf :as kdf] |
|
[clojure.java.io :as io] |
|
[clojure.java.shell :as sh] |
|
[clojure.pprint :as pprint] |
|
[clojure.string :as string]) |
|
(:import (java.util Base64))) |
|
|
|
|
|
(set! *warn-on-reflection* true) |
|
|
|
|
|
(defn bytes->b64 [^bytes b] (String. (.encode (Base64/getEncoder) b))) |
|
(defn b64->bytes [^String s] (.decode (Base64/getDecoder) (.getBytes s))) |
|
|
|
|
|
(defn slow-key-stretch-with-pbkdf2 [weak-text-key n-bytes] |
|
(kdf/get-bytes |
|
(kdf/engine {:key weak-text-key |
|
:salt (b64->bytes "j3gT0zoPJos=") |
|
:alg :pbkdf2 |
|
:digest :sha512 |
|
:iterations 1e5}) ;; target O(100ms) on commodity hardware |
|
n-bytes)) |
|
|
|
|
|
(defn encrypt |
|
"Encrypt and return a {:data <b64>, :iv <b64>} that can be decrypted with the |
|
same `password`. |
|
|
|
Performs pbkdf2 key stretching with quite a few iterations on `password`." |
|
[clear-text password] |
|
(let [initialization-vector (nonce/random-bytes 16)] |
|
{:data (bytes->b64 |
|
(crypto/encrypt |
|
(codecs/to-bytes clear-text) |
|
(slow-key-stretch-with-pbkdf2 password 64) |
|
initialization-vector |
|
{:algorithm :aes256-cbc-hmac-sha512})) |
|
:iv (bytes->b64 initialization-vector)})) |
|
|
|
|
|
(defn decrypt |
|
"Decrypt and return the clear text for some output of `encrypt` given the |
|
same `password` used during encryption." |
|
[{:keys [data iv]} password] |
|
(codecs/bytes->str |
|
(crypto/decrypt |
|
(b64->bytes data) |
|
(slow-key-stretch-with-pbkdf2 password 64) |
|
(b64->bytes iv) |
|
{:algorithm :aes256-cbc-hmac-sha512}))) |
|
|
|
|
|
(comment |
|
;; Sufficiently slow to protect against brute force |
|
|
|
(time (encrypt "some clear text" "my password")) |
|
; "Elapsed time: 245.778331 msecs" |
|
;=> {:data "2jttNkz8Uk2kQ7kyRMIyPSIYZRRyAa/+ACtjP+8M4w64Bp4tE2pyVNQV299EFsSJ", |
|
; :iv "m8r6cuQICvlWjobE6sE7XQ=="} |
|
|
|
(time (decrypt *1 "my password")) |
|
; "Elapsed time: 188.044263 msecs" |
|
;=> "some clear text" |
|
) |
|
|
|
|
|
(defn read-password |
|
([] (read-password nil)) |
|
([prompt] |
|
(if-let [console (System/console)] |
|
(do |
|
(when prompt (print prompt) (flush)) |
|
(String. (.readPassword console))) |
|
(do |
|
(println "[WARN] No secure console available, reading via plaintext.") |
|
(when prompt (print prompt) (flush)) |
|
(read-line))))) |
|
|
|
|
|
(defn encrypt-to-disk |
|
[edn-compliant-data {:keys [password path]}] |
|
(let [f (io/file path)] |
|
(io/make-parents f) |
|
(let [encrypted (encrypt (pr-str edn-compliant-data) password)] |
|
(spit f (prn-str encrypted)) |
|
encrypted))) |
|
|
|
|
|
(defn decrypt-from-disk |
|
[{:keys [password path]}] |
|
(let [f (io/file path)] |
|
(if-not (.isFile f) |
|
{} |
|
(-> (slurp f) |
|
(read-string) |
|
(decrypt password) |
|
(read-string))))) |
|
|
|
|
|
;;; Helpers that assume you want to store your keys at kpath |
|
|
|
|
|
(def ^:private kpath "conf/keys.edn") |
|
|
|
|
|
(defn- read-keys [] |
|
(if (.isFile (io/file kpath)) |
|
(let [p (read-password "Password: ")] |
|
{:data (decrypt-from-disk {:password p :path "conf/keys.edn"}) |
|
:password p}) |
|
{:data {} |
|
:password nil})) |
|
|
|
|
|
(defn- update-password? [{:keys [password] :as x}] |
|
(let [change? (or |
|
(nil? password) |
|
(do (print "Encrypt with a new password? [y/N] ") |
|
(flush) |
|
(= (string/lower-case (read-line)) "y")))] |
|
(if change? |
|
(let [p (read-password "Set password: ") |
|
p' (read-password "Confirm password: ")] |
|
(if (= p p') |
|
(assoc x :password p) |
|
(do |
|
(println "Passwords didn't match.") |
|
(recur (assoc x :password nil))))) |
|
x))) |
|
|
|
|
|
(defn- edit-keys [{:keys [data] :as x}] |
|
(let [pprinted (with-out-str (pprint/pprint data)) |
|
{:keys [exit out err] :as data} |
|
(try |
|
(sh/sh "vipe" :in pprinted) |
|
(catch Exception e |
|
(assoc (ex-data e) |
|
:exit 1 :err (ex-message e))))] |
|
|
|
(when-not (= exit 0) |
|
(println "Failed to open decrypted keys with `vipe`!") |
|
(println " Maybe try $ apt install moreutils") |
|
(println err) |
|
(throw (ex-info err data))) |
|
|
|
(assoc x :data (read-string out)))) |
|
|
|
|
|
(defn- write-keys [{:keys [data password]}] |
|
(print "Encrypting data for writing...") |
|
(flush) |
|
(let [enc (encrypt-to-disk data {:password password :path kpath})] |
|
(println " Done.") |
|
(println "Wrote" (count (.getBytes (prn-str enc))) "bytes.") |
|
enc)) |
|
|
|
|
|
(defn edit-keys! |
|
"Run from a terminal (maybe via a lein alias) to edit |
|
the encrypted API keys with the default text editor." |
|
[] |
|
(-> (read-keys) |
|
(update-password?) |
|
(edit-keys) |
|
(write-keys)) |
|
(System/exit 0)) |
|
|
|
|
|
(defn read-keys! |
|
"Read & decrypt the API keys from disk. |
|
|
|
Call this once at the top level of the application, right |
|
when it starts up." |
|
[] |
|
(:data (read-keys))) |