From 2dec26fd955970a4425f2509b6ac9592b566c5c0 Mon Sep 17 00:00:00 2001 From: Peter Taoussanis Date: Sat, 8 Sep 2018 15:11:50 +0200 Subject: [PATCH] [Encryption] Add a new (private, alpha) `taoensso.nippy.crypto` ns with low-level utils Specifically: - Exposes ability to use arb crypto algorithm - Exposes ability to use arb key function - Supports explicit salts (incl. variable salt length) - Supports arbitrary key length (e.g. AES 256) - Defaults to AES/GCM/NoPadding algorithm --- src/taoensso/nippy/crypto.clj | 127 ++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 src/taoensso/nippy/crypto.clj diff --git a/src/taoensso/nippy/crypto.clj b/src/taoensso/nippy/crypto.clj new file mode 100644 index 0000000..1ee0137 --- /dev/null +++ b/src/taoensso/nippy/crypto.clj @@ -0,0 +1,127 @@ +(ns taoensso.nippy.crypto + "Low-level crypto utils. + Private & alpha, very likely to change!" + (:require [taoensso.encore :as enc])) + +;; Note that AES128 may be preferable to AES256 due to known attack +;; vectors specific to AES256, Ref. https://goo.gl/qU4CCV +;; or for a counter argument, Ref. https://goo.gl/9LA9Yb + +;;;; Randomness + +(do + (def ^:private prng* (enc/thread-local-proxy (java.security.SecureRandom/getInstance "SHA1PRNG"))) + (defn prng ^java.security.SecureRandom [] (.get ^ThreadLocal prng*)) + (defn rand-bytes [size] (let [ba (byte-array size)] (.nextBytes (prng) ba) ba))) + +(comment (seq (rand-bytes 16))) + +;;;; Key derivation (salt+password -> key / hash) +;; (fn [salt-ba utf8]) -> bytes + +;; (defn ba->hex [^bytes ba] (org.apache.commons.codec.binary.Hex/encodeHexString ba)) +(defn take-ba [n ^bytes ba] (java.util.Arrays/copyOf ba ^int n)) ; Pads if ba too small +(defn utf8->ba [^String s] (.getBytes s "UTF-8")) + +(def ^:private sha512-md* (enc/thread-local-proxy (java.security.MessageDigest/getInstance "SHA-512"))) +(defn sha512-md ^java.security.MessageDigest [] (.get ^ThreadLocal sha512-md*)) +(defn sha512-ba + "SHA512-based key generator. Good JVM availability without extra dependencies + (PBKDF2, bcrypt, scrypt, etc.). Decent security when using many rounds." + ([salt-ba utf8 ] (sha512-ba salt-ba utf8 (* Short/MAX_VALUE 5))) + ([salt-ba ^String utf8 ^long n-rounds] + (let [md (sha512-md) + ba (let [pwd-ba (.getBytes utf8 "UTF-8")] + (if salt-ba + (enc/ba-concat salt-ba pwd-ba) + pwd-ba))] + + (enc/reduce-n (fn [acc in] (.digest md acc)) ba n-rounds)))) + +(comment + (count (seq (sha512-ba (utf8->ba "salt") "password" 1))) + (count (seq (sha512-ba nil "password" 1)))) + +;;;; Crypto + +(defprotocol ICipherKit + (get-cipher ^javax.crypto.Cipher [_] "Returns a thread-safe `javax.crypto.Cipher` instance.") + (get-iv-size [_] "Returns necessary iv-ba length.") + (get-key-spec ^javax.crypto.spec.SecretKeySpec [_ ba] "Returns a `javax.crypto.spec.SecretKeySpec`.") + (get-param-spec ^java.security.spec.AlgorithmParameterSpec [_ iv-ba] "Returns a `java.security.spec.AlgorithmParameters`.")) + +;; Prefer GCM > CBC, Ref. https://goo.gl/jpZoj8 +(def ^:private gcm-cipher* (enc/thread-local-proxy (javax.crypto.Cipher/getInstance "AES/GCM/NoPadding"))) +(def ^:private cbc-cipher* (enc/thread-local-proxy (javax.crypto.Cipher/getInstance "AES/CBC/PKCS5Padding"))) + +(defn gcm-cipher ^javax.crypto.Cipher [] (.get ^ThreadLocal gcm-cipher*)) +(defn cbc-cipher ^javax.crypto.Cipher [] (.get ^ThreadLocal cbc-cipher*)) +; +(deftype CipherKit-AES-GCM [] + ICipherKit + (get-cipher [_] (gcm-cipher)) + (get-iv-size [_] 12) + (get-key-spec [_ ba] (javax.crypto.spec.SecretKeySpec. ba "AES")) + (get-param-spec [_ iv-ba] (javax.crypto.spec.GCMParameterSpec. 128 iv-ba))) + +(deftype CipherKit-AES-CBC [] + ICipherKit + (get-cipher [_] (cbc-cipher)) + (get-iv-size [_] 16) + (get-key-spec [_ ba] (javax.crypto.spec.SecretKeySpec. ba "AES")) + (get-param-spec [_ iv-ba] (javax.crypto.spec.IvParameterSpec. iv-ba))) + +(def cipher-kit-aes-gcm "Default CipherKit for AES GCM" (CipherKit-AES-GCM.)) +(def cipher-kit-aes-cbc "Default CipherKit for AES CBC" (CipherKit-AES-CBC.)) + +;; Output bytes: [ ] +;; Could also do: [ ] +(defn encrypt [cipher-kit ?salt-ba key-ba ba] + (let [iv-size (long (get-iv-size cipher-kit)) + iv-ba (rand-bytes iv-size) + prefix-ba (if ?salt-ba (enc/ba-concat iv-ba ?salt-ba) iv-ba) + key-spec (get-key-spec cipher-kit key-ba) + param-spec (get-param-spec cipher-kit iv-ba) + cipher (get-cipher cipher-kit)] + + (.init cipher javax.crypto.Cipher/ENCRYPT_MODE key-spec param-spec) + (enc/ba-concat prefix-ba (.doFinal cipher ba)))) + +(comment (encrypt cipher-kit-aes-gcm nil (take-ba 16 (sha512-ba nil "pwd")) (utf8->ba "data"))) + +(defn decrypt [cipher-kit salt-size salt->key-fn ba] + (let [salt-size (long salt-size) + iv-size (long (get-iv-size cipher-kit)) + prefix-size (+ iv-size salt-size) + [prefix-ba data-ba] (enc/ba-split ba prefix-size) + [iv-ba salt-ba] (if (pos? salt-size) + (enc/ba-split prefix-ba iv-size) + [prefix-ba nil]) + + key-ba (salt->key-fn salt-ba) + key-spec (get-key-spec cipher-kit key-ba) + param-spec (get-param-spec cipher-kit iv-ba) + cipher (get-cipher cipher-kit)] + + (.init cipher javax.crypto.Cipher/DECRYPT_MODE key-spec param-spec) + (.doFinal cipher data-ba))) + +(comment + (do + (defn sha512-16 [?salt-ba pwd] (take-ba 16 (sha512-ba ?salt-ba pwd))) + (defn roundtrip [kit ?salt-ba key-ba key-fn] + (let [salt-size (count ?salt-ba) + encr (encrypt kit ?salt-ba key-ba (utf8->ba "data")) + decr (decrypt kit salt-size key-fn encr)] + (String. ^bytes decr "UTF-8"))) + + [(let [s (rand-bytes 16)] (roundtrip cipher-kit-aes-gcm s (sha512-16 s "pwd") #(sha512-16 % "pwd"))) + (let [s nil] (roundtrip cipher-kit-aes-gcm s (sha512-16 s "pwd") #(sha512-16 % "pwd"))) + (let [s (rand-bytes 16)] (roundtrip cipher-kit-aes-cbc s (sha512-16 s "pwd") #(sha512-16 % "pwd"))) + (let [s nil] (roundtrip cipher-kit-aes-cbc s (sha512-16 s "pwd") #(sha512-16 % "pwd")))]) + + (enc/qb 10 + (roundtrip "foo" {}) + (roundtrip "foo" {:cipher-kit cipher-kit-aes-cbc})) + ;; [2348.05 2332.25] + )