Crypto: simplify design, add auto salting

Have decided to simplify the API even further and bring configuration down to
essentially one decision: do you want auto salting, or key caching?
This commit is contained in:
Peter Taoussanis 2013-06-11 23:03:30 +07:00
parent bea3f5e84e
commit 4ac2a34d7a
7 changed files with 143 additions and 112 deletions

View file

@ -2,7 +2,7 @@ Current [semantic](http://semver.org/) version:
```clojure ```clojure
[com.taoensso/nippy "1.2.1"] ; Stable [com.taoensso/nippy "1.2.1"] ; Stable
[com.taoensso/nippy "1.3.0-alpha1"] ; Development (adds crypto support) [com.taoensso/nippy "1.3.0-alpha2"] ; Development (adds crypto support)
``` ```
# Nippy, a Clojure serialization library # Nippy, a Clojure serialization library
@ -18,7 +18,7 @@ Nippy is an attempt to provide a drop-in, high-performance alternative to the re
* **Reader-fallback** for difficult/future types (including Clojure 1.4+ tagged literals). * **Reader-fallback** for difficult/future types (including Clojure 1.4+ tagged literals).
* **Full test coverage** for every supported type. * **Full test coverage** for every supported type.
* [Snappy](http://code.google.com/p/snappy/) **integrated de/compression** for efficient storage and network transfer. * [Snappy](http://code.google.com/p/snappy/) **integrated de/compression** for efficient storage and network transfer.
* Enable **high-strength encryption** with a single `:password "my-password"` option. (1.3.0+) * Enable **high-strength encryption** with a single `:password [:salted "my-password"]` option. (1.3.0+)
## Getting started ## Getting started

View file

@ -1,4 +1,4 @@
(defproject com.taoensso/nippy "1.3.0-alpha1" (defproject com.taoensso/nippy "1.3.0-alpha2"
:description "Clojure serialization library" :description "Clojure serialization library"
:url "https://github.com/ptaoussanis/nippy" :url "https://github.com/ptaoussanis/nippy"
:license {:name "Eclipse Public License" :license {:name "Eclipse Public License"

View file

@ -167,16 +167,15 @@
(defn freeze-to-bytes (defn freeze-to-bytes
"Serializes x to a byte array and returns the array." "Serializes x to a byte array and returns the array."
^bytes [x & {:keys [crypto compress? print-dup? salt password] ^bytes [x & {:keys [compress? print-dup? password]
:or {crypto crypto/crypto-default :or {compress? true
compress? true
print-dup? true}}] print-dup? true}}]
(let [ba (ByteArrayOutputStream.) (let [ba (ByteArrayOutputStream.)
stream (DataOutputStream. ba)] stream (DataOutputStream. ba)]
(freeze-to-stream! stream x print-dup?) (freeze-to-stream! stream x print-dup?)
(let [ba (.toByteArray ba) (let [ba (.toByteArray ba)
ba (if compress? (utils/compress-bytes ba) ba) ba (if compress? (utils/compress-bytes ba) ba)
ba (if password (crypto/encrypt crypto salt password ba) ba)] ba (if password (crypto/encrypt-aes128 password ba) ba)]
ba))) ba)))
;;;; Thawing ;;;; Thawing
@ -255,11 +254,11 @@
(defn thaw-from-bytes (defn thaw-from-bytes
"Deserializes an object from given byte array." "Deserializes an object from given byte array."
[ba & {:keys [crypto compressed? read-eval? salt password] [ba & {:keys [compressed? read-eval? password]
:or {crypto crypto/crypto-default :or {compressed? true
read-eval? false ; For `read-string` injection safety - NB!!! read-eval? false ; For `read-string` injection safety - NB!!!
compressed? true}}] }}]
(-> (let [ba (if password (crypto/decrypt crypto salt password ba) ba) (-> (let [ba (if password (crypto/decrypt-aes128 password ba) ba)
ba (if compressed? (utils/uncompress-bytes ba) ba)] ba (if compressed? (utils/uncompress-bytes ba) ba)]
ba) ba)
(ByteArrayInputStream.) (ByteArrayInputStream.)

View file

@ -13,11 +13,9 @@
(defn reader-thaw [x] (binding [*read-eval* false] (read-string x))) (defn reader-thaw [x] (binding [*read-eval* false] (read-string x)))
(def reader-roundtrip (comp reader-thaw reader-freeze)) (def reader-roundtrip (comp reader-thaw reader-freeze))
(def crypto-opts [:password "secret" :crypto crypto/crypto-default-cached])
(def roundtrip-defaults (comp nippy/thaw-from-bytes nippy/freeze-to-bytes)) (def roundtrip-defaults (comp nippy/thaw-from-bytes nippy/freeze-to-bytes))
(def roundtrip-encrypted (comp #(apply nippy/thaw-from-bytes % crypto-opts) (def roundtrip-encrypted (comp #(nippy/thaw-from-bytes % :password [:cached "p"])
#(apply nippy/freeze-to-bytes % crypto-opts))) #(nippy/freeze-to-bytes % :password [:cached "p"])))
(def roundtrip-fast (comp #(nippy/thaw-from-bytes % :compressed? false) (def roundtrip-fast (comp #(nippy/thaw-from-bytes % :compressed? false)
#(nippy/freeze-to-bytes % :compress? false))) #(nippy/freeze-to-bytes % :compress? false)))
@ -48,11 +46,11 @@
(println (println
{:encrypted {:encrypted
{:freeze (bench (apply freeze-to-bytes data crypto-opts)) {:freeze (bench (freeze-to-bytes data :password [:cached "p"]))
:thaw (let [frozen (apply freeze-to-bytes data crypto-opts)] :thaw (let [frozen (freeze-to-bytes data :password [:cached "p"])]
(bench (apply thaw-from-bytes frozen crypto-opts))) (bench (thaw-from-bytes frozen :password [:cached "p"])))
:round (bench (roundtrip-encrypted data)) :round (bench (roundtrip-encrypted data))
:data-size (count (apply freeze-to-bytes data crypto-opts))}}) :data-size (count (freeze-to-bytes data :password [:cached "p"]))}})
(println (println
{:fast {:fast
@ -66,9 +64,9 @@
;;; 11 June 2013: Clojure 1.5.1, Nippy 1.3.0-alpha1 ;;; 11 June 2013: Clojure 1.5.1, Nippy 1.3.0-alpha1
;; {:reader {:freeze 17042, :thaw 31579, :round 48379, :data-size 22954}} ;; {:reader {:freeze 17042, :thaw 31579, :round 48379, :data-size 22954}}
;; {:fast {:freeze 3078, :thaw 4684, :round 8117, :data-size 13274}}
;; {:defaults {:freeze 3810, :thaw 5295, :round 9052, :data-size 12394}} ;; {:defaults {:freeze 3810, :thaw 5295, :round 9052, :data-size 12394}}
;; {:encrypted {:freeze 5800, :thaw 6862, :round 12317, :data-size 12416}} ;; {:encrypted {:freeze 5800, :thaw 6862, :round 12317, :data-size 12416}}
;; {:fast {:freeze 3078, :thaw 4684, :round 8117, :data-size 13274}}
;;; Clojure 1.5.1, Nippy 1.2.1 (+ sorted-set, sorted-map) ;;; Clojure 1.5.1, Nippy 1.2.1 (+ sorted-set, sorted-map)
;; (def data (dissoc data :sorted-set :sorted-map)) ;; (def data (dissoc data :sorted-set :sorted-map))

View file

@ -3,113 +3,145 @@
Simple no-nonsense crypto with reasonable defaults. Because your Clojure data Simple no-nonsense crypto with reasonable defaults. Because your Clojure data
deserves some privacy." deserves some privacy."
{:author "Peter Taoussanis"} {:author "Peter Taoussanis"}
(:require [taoensso.nippy.utils :as utils])) (:require [clojure.string :as str]
[taoensso.nippy.utils :as utils]))
(defprotocol ICrypto "Simple cryptography interface." ;;;; Interface
(gen-key ^javax.crypto.spec.SecretKeySpec [crypto salt pwd]
"Returns an appropriate SecretKeySpec.")
(encrypt ^bytes [crypto salt pwd ba] "Returns encrypted bytes.")
(decrypt ^bytes [crypto salt pwd ba] "Returns decrypted bytes."))
(defrecord CryptoAES [cipher-type default-salt key-gen-opts cache]) (defprotocol IEncrypter
(gen-key ^javax.crypto.spec.SecretKeySpec [encrypter salt-ba pwd])
(encrypt ^bytes [encrypter pwd ba])
(decrypt ^bytes [encrypter pwd ba]))
(def ^:private ^java.security.MessageDigest sha-md (defrecord AES128Encrypter [key-work-factor key-cache])
(java.security.MessageDigest/getInstance "SHA-512"))
(def ^:private ^:const aes128-block-size (int 16)) ;;;; Digests, ciphers, etc.
(defn- sha512-key
"Default SHA512-based key generator. Good JVM availability without extra
dependencies (PBKDF2, bcrypt, scrypt, etc.). Decent security with multiple
rounds. VERY aggressive multiples (>64) possible+recommended when cached."
[^String salted-pwd & [{:keys [rounds-multiple]
:or {rounds-multiple 5}}]] ; Cacheable
(loop [^bytes ba (.getBytes salted-pwd "UTF-8")
n (* (int Short/MAX_VALUE) (or rounds-multiple 5))]
(if-not (zero? n)
(recur (.digest sha-md ba) (dec n))
(-> ba
;; 128bit keys have good JVM availability and are ;; 128bit keys have good JVM availability and are
;; entirely sufficient, Ref. http://goo.gl/2YRQG ;; entirely sufficient, Ref. http://goo.gl/2YRQG
(java.util.Arrays/copyOf aes128-block-size) (def ^:private ^:const aes128-block-size (int 16))
(def ^:private ^:const salt-size (int 16))
(def ^:private ^javax.crypto.Cipher aes128-cipher
(javax.crypto.Cipher/getInstance "AES/CBC/PKCS5Padding"))
(def ^:private ^java.security.MessageDigest sha512-md
(java.security.MessageDigest/getInstance "SHA-512"))
(def ^:private ^java.security.SecureRandom prng
(java.security.SecureRandom/getInstance "SHA1PRNG"))
(defn- rand-bytes [size] (let [seed (byte-array size)] (.nextBytes prng seed) seed))
;;;; Default keygen
(defn- sha512-key
"SHA512-based key generator. Good JVM availability without extra dependencies
(PBKDF2, bcrypt, scrypt, etc.). Decent security with multiple rounds."
[salt-ba ^String pwd key-work-factor]
(loop [^bytes ba (let [pwd-ba (.getBytes pwd "UTF-8")]
(if salt-ba (utils/ba-concat salt-ba pwd-ba) pwd-ba))
n (* (int Short/MAX_VALUE) key-work-factor)]
(if-not (zero? n)
(recur (.digest sha512-md ba) (dec n))
(-> ba (java.util.Arrays/copyOf aes128-block-size)
(javax.crypto.spec.SecretKeySpec. "AES"))))) (javax.crypto.spec.SecretKeySpec. "AES")))))
(comment (comment
(time (sha512-key "hi" {:rounds-multiple 1})) ; ~40ms per hash (fast) (time (sha512-key nil "hi" 1)) ; ~40ms per hash (fast)
(time (sha512-key "hi" {:rounds-multiple 5})) ; ~180ms (default) (time (sha512-key nil "hi" 5)) ; ~180ms (default)
(time (sha512-key "hi" {:rounds-multiple 32})) ; ~1200ms (conservative) (time (sha512-key nil "hi" 32)) ; ~1200ms (conservative)
(time (sha512-key "hi" {:rounds-multiple 128})) ; ~4500ms (paranoid) (time (sha512-key nil "hi" 128)) ; ~4500ms (paranoid)
) )
(def ^:private cipher* (memoize #(javax.crypto.Cipher/getInstance %))) ;;;; Default implementation
(defn- cipher ^javax.crypto.Cipher [cipher-type] (cipher* cipher-type))
(def ^:private ^java.security.SecureRandom rand-gen (extend-type AES128Encrypter
(java.security.SecureRandom/getInstance "SHA1PRNG")) IEncrypter
(defn- rand-bytes [size] (let [seed (make-array Byte/TYPE size)] (gen-key [{:keys [key-work-factor key-cache]} salt-ba pwd]
(.nextBytes rand-gen seed) seed)) ;; Trade-off: salt-ba and key-cache mutually exclusive
(utils/memoized key-cache sha512-key salt-ba pwd key-work-factor))
(extend-type CryptoAES (encrypt [{:keys [key-cache] :as this} pwd data-ba]
ICrypto (let [salt? (not key-cache)
(gen-key [{:keys [default-salt key-gen-opts cache]} salt pwd]
(utils/apply-memoized cache
sha512-key (str (or salt default-salt) pwd) key-gen-opts))
(encrypt [{:keys [cipher-type cache] :as crypto} salt pwd ba]
(let [cipher (cipher cipher-type)
key (gen-key crypto salt pwd)
iv-ba (rand-bytes aes128-block-size) iv-ba (rand-bytes aes128-block-size)
salt-ba (when salt? (rand-bytes salt-size))
prefix-ba (if-not salt? iv-ba (utils/ba-concat iv-ba salt-ba))
key (gen-key this salt-ba pwd)
iv (javax.crypto.spec.IvParameterSpec. iv-ba)] iv (javax.crypto.spec.IvParameterSpec. iv-ba)]
(.init cipher javax.crypto.Cipher/ENCRYPT_MODE key iv) (.init aes128-cipher javax.crypto.Cipher/ENCRYPT_MODE key iv)
(utils/ba-concat iv-ba (.doFinal cipher ba)))) (utils/ba-concat prefix-ba (.doFinal aes128-cipher data-ba))))
(decrypt [{:keys [cipher-type cache] :as crypto} salt pwd ba] (decrypt [{:keys [key-cache] :as this} pwd ba]
(let [cipher (cipher cipher-type) (let [salt? (not key-cache)
key (gen-key crypto salt pwd) prefix-size (+ aes128-block-size (if salt? salt-size 0))
[iv-ba data-ba] (utils/ba-split ba aes128-block-size) [prefix-ba data-ba] (utils/ba-split ba prefix-size)
[iv-ba salt-ba] (if-not salt? [prefix-ba nil]
(utils/ba-split prefix-ba aes128-block-size))
key (gen-key this salt-ba pwd)
iv (javax.crypto.spec.IvParameterSpec. iv-ba)] iv (javax.crypto.spec.IvParameterSpec. iv-ba)]
(.init cipher javax.crypto.Cipher/DECRYPT_MODE key iv) (.init aes128-cipher javax.crypto.Cipher/DECRYPT_MODE key iv)
(.doFinal cipher data-ba)))) (.doFinal aes128-cipher data-ba))))
(defn crypto-aes128 (def aes128-salted
"Returns a new CryptoAES object with options: "USE CASE: You want more than a small, finite number of passwords (e.g. each
:default-salt - Shared fallback password salt when none is provided. If item encrypted will use a unique user-provided password).
the use case allows it, a unique random salt per
encrypted item is better.
:cache-keys? - IMPORTANT. DO enable this if and ONLY if your use case
involves only a small, finite number of unique secret
keys (salt+password)s. Dramatically improves `gen-key`
performance in those cases and (as a result) allows for
a *much* stronger `key-work-factor`.
:key-work-factor - O(n) CPU time needed to generate keys. Larger factors
provide more protection against brute-force attacks but
make encryption+decryption slower if `:cache-keys?` is
not enabled.
Some sensible values (from fast to strong): IMPLEMENTATION: Uses a relatively cheap key hash, but automatically salts
Without caching: 1, 5, 10 every key.
With caching: 5, 32, 64, 128
See also `crypto-default` and `crypto-default-cached` for sensible ready-made PROS: Each key is independent so would need to be attacked independently.
CryptoAES objects." CONS: Key caching impossible, so there's an inherent trade-off between
[& [{:keys [default-salt cache-keys? key-work-factor] encryption/decryption speed and the difficulty of attacking any
:or {default-salt "XA~I3(:]3'ck5!M[z\\m`l^0mltR~y/]Arq_d9+$`e#yJssN^8" particular key.
key-work-factor 5}}]]
(CryptoAES. "AES/CBC/PKCS5Padding"
default-salt
{:rounds-multiple (int key-work-factor)}
(when cache-keys? (atom {}))))
(def crypto-default (crypto-aes128)) Slower than `aes128-cached`, and easier to attack any particular key."
(def crypto-default-cached (crypto-aes128 {:cache-keys? true (AES128Encrypter. 5 nil))
:key-work-factor 64}))
(def aes128-cached
"USE CASE: You want only a small, finite number of passwords (e.g. a limited
number of staff/admins, or you'll be using a single password to
encrypt many items).
IMPLEMENTATION: Uses a _very_ expensive (but cached) key hash, and no salt.
PROS: Great amortized encryption/decryption speed. Expensive key hash makes
attacking any particular key very difficult.
CONS: Using a small number of keys for many encrypted items means that if any
key _is_ somehow compromised, _all_ items encrypted with that key are
compromised.
Faster than `aes128-salted`, and harder to attack any particular key - but
increased danger if a key is somehow compromised."
(AES128Encrypter. 64 (atom {})))
(defn- destructure-typed-password
"[<type> <password>] -> [Encrypter <password>]"
[typed-password]
(letfn [(throw-ex []
(throw (Exception.
(str "Expected password form: "
"[<#{:salted :cached}> <password-string>].\n "
"See `aes128-salted`, `aes128-cached` for details."))))]
(if-not (vector? typed-password)
(throw-ex)
(let [[type password] typed-password]
[(case type :salted aes128-salted :cached aes128-cached (throw-ex))
password]))))
(defn encrypt-aes128 [typed-password ba]
(let [[encrypter password] (destructure-typed-password typed-password)]
(encrypt encrypter password ba)))
(defn decrypt-aes128 [typed-password ba]
(let [[encrypter password] (destructure-typed-password typed-password)]
(decrypt encrypter password ba)))
(comment (comment
(time (gen-key crypto-default "my-salt" "my-password")) (encrypt-aes128 "my-password" (.getBytes "Secret message")) ; Malformed
(time (gen-key crypto-default-cached "my-salt" "my-password")) (time (gen-key aes128-salted nil "my-password"))
(time (gen-key aes128-cached nil "my-password"))
(time (->> (.getBytes "Secret message" "UTF-8") (time (->> (.getBytes "Secret message" "UTF-8")
(encrypt crypto-default "s" "p") (encrypt-aes128 [:salted "p"])
(encrypt crypto-default "s" "p") (encrypt-aes128 [:cached "p"])
(decrypt crypto-default "s" "p") (decrypt-aes128 [:cached "p"])
(decrypt crypto-default "s" "p") (decrypt-aes128 [:salted "p"])
(String.)))) (String.))))

View file

@ -61,9 +61,9 @@
(defn compress-bytes [^bytes ba] (Snappy/compress ba)) (defn compress-bytes [^bytes ba] (Snappy/compress ba))
(defn uncompress-bytes [^bytes ba] (Snappy/uncompress ba 0 (alength ba))) (defn uncompress-bytes [^bytes ba] (Snappy/uncompress ba 0 (alength ba)))
(defn apply-memoized (defn memoized
"A cross between `memoize` and `apply`. Operates like `apply` but accepts an "Like `memoize` but takes an explicit cache atom (possibly nil) and
optional {<args> <value> ...} cache atom." immediately applies memoized f to given arguments."
[cache f & args] [cache f & args]
(if-not cache (if-not cache
(apply f args) (apply f args)
@ -73,6 +73,9 @@
(swap! cache assoc args dv) (swap! cache assoc args dv)
@dv)))) @dv))))
(comment (memoized nil +)
(memoized nil + 5 12))
(defn ba-concat ^bytes [^bytes ba1 ^bytes ba2] (defn ba-concat ^bytes [^bytes ba1 ^bytes ba2]
(let [s1 (alength ba1) (let [s1 (alength ba1)
s2 (alength ba2) s2 (alength ba2)

View file

@ -7,9 +7,8 @@
(def test-data (dissoc nippy/stress-data :bytes)) (def test-data (dissoc nippy/stress-data :bytes))
(def roundtrip-defaults (comp nippy/thaw-from-bytes nippy/freeze-to-bytes)) (def roundtrip-defaults (comp nippy/thaw-from-bytes nippy/freeze-to-bytes))
(def roundtrip-encrypted (comp #(nippy/thaw-from-bytes % :password "secret") (def roundtrip-encrypted (comp #(nippy/thaw-from-bytes % :password [:cached "secret"])
#(nippy/freeze-to-bytes % :password "secret"))) #(nippy/freeze-to-bytes % :password [:cached "secret"])))
(deftest test-roundtrip-defaults (is (= test-data (roundtrip-defaults test-data)))) (deftest test-roundtrip-defaults (is (= test-data (roundtrip-defaults test-data))))
(deftest test-roundtrip-encrypted (is (= test-data (roundtrip-encrypted test-data)))) (deftest test-roundtrip-encrypted (is (= test-data (roundtrip-encrypted test-data))))