Skip to content

Commit

Permalink
v0.2.0: JVM and ClojureScript support for AES-GCM encryption
Browse files Browse the repository at this point in the history
  • Loading branch information
skinkade committed Sep 8, 2021
1 parent 21232d6 commit 0f52600
Show file tree
Hide file tree
Showing 20 changed files with 867 additions and 10 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Change Log
All notable changes to this project will be documented in this file. This change log follows the conventions of [keepachangelog.com](http://keepachangelog.com/).

## [0.2.0]
### Added
JVM and ClojureScript support for AES-GCM encryption with a high-level API

## [0.1.1]
### Changed
- Use goog.crypt in internals.util-js in place of base64-js dependency and homebrew hex encode
Expand All @@ -16,5 +20,6 @@ JVM and ClojureScript support for cryptographically-random:
- collection samples
- passwords/passphrases

[0.2.0]: https://github.com/skinkade/uniformity/compare/v0.1.1...v0.2.0
[0.1.1]: https://github.com/skinkade/uniformity/compare/v0.1.0...v0.1.1
[0.1.0]: https://github.com/skinkade/uniformity/releases/tag/v0.1.0
29 changes: 28 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,41 @@ Within ClojureScript, it's backed by either `window.crypto.getRandomValues` in-b
or `crypto.randomBytes` in Node.js.



## Modules

### Crypto
`uniformity.crypto` provides a high-level, easy-to-use API for encrypting bytes
and strings with AES-GCM, using passwords and/or keys.
Passwords are treated as UTF-8 strings and processed with 100,000 rounds of
PBKDF2-HMAC-SHA256 by default.
[See documentation](doc/crypto.md).

```clojure
;; password
(def password "Strong password")
(def encrypted (encrypt "Secret text" password))

;; key
(def secret-key (rand-bytes 16))
(def encrypted (encrypt "Secret text" secret-key))

;; or both...
(def encrypted (encrypt "Secret text" [password secret-key]))

;; ... and any key supplied works to decrypt
(decrypt encrypted password)
(decrypt encrypted secret-key)
```

### Random
`uniformity.random` contains functions for generating crytographically random
booleans, bytes, 32-bit integers, base64/hex-encoded strings, UUIDs,
collection samples, and passwords/passphrases.
[See documentation](doc/random.md).

`uniformity.util`, at the moment, is largely for encoding/decoding base64/hex.
`uniformity.util`, at the moment, is largely for encoding/decoding base64/hex,
JSON serialization, and UTF-8 string encoding/decoding.
Input of encoding functions and output of decoding functions are byte arrays
(byte[] on JVM, Uint8Array in JS).
[See documentation](doc/util.md).
3 changes: 3 additions & 0 deletions doc/cljdoc.edn
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
{:cljdoc.doc/tree [["Readme" {:file "README.md"}]
["Crypto" {:file "doc/crypto.md"}
["A note on GCM nonces" {:file "doc/crypto/gcm_nonce_note.md"}]
["Low level usage", {:file "doc/crypto/low_level_usage.md"}]]
["Random" {:file "doc/random.md"}]
["Util" {:file "doc/util.md"}]]}
183 changes: 183 additions & 0 deletions doc/crypto.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
# Symmetric Encryption

`uniformity` provides easy-to-use encryption functionality using AES128-GCM
and a self-describing output format, refered to as a 'cryptopack'.

User-provided keys and passwords are never directly used to encrypt user-provided
plaintext. Rather, a unique data encryption key is generated for each piece of
data, and user-provided keys and passwords are used for key encryption keys.
Encrypted copies of the data encryption key are stored in key slots within the
metadata alongside the ciphertext.

Allowed length for keys are 128, 192, and 256 bits.
Nonces are 96 bits, and authentication tags are 128 bits.

For more implementation details and rationale, see associated articles.

Note that the pretty-printed vectors of integers below are in reality byte arrays
(byte[] on JVM, Uint8Array in JS).

Plaintext strings are treated as UTF-8 bytes.



### Flags to know about

`encrypt` can take two additional flags:
- `:json` makes a compact cryptopack and serializes it to JSON
- `:padded` - while AES-GCM internally pads plaintext to intervals of 16 bytes,
this flag enables pre-emptive padding so that the length of the ciphertext
does not match that of the plaintext



## Examples

```clojure
(ns com.example
(:require [uniformity.crypto :refer [encrypt
decrypt]]
[uniformity.random :refer [rand-bytes]]
[uniformity.util :refer [hex-decode]]
[clojure.pprint :refer [pprint]]))
```

### Password-based encryption

`uniformity` defaults to using PBKDF2-HMAC-SHA256 with 100,000 rounds and a
random 128-bit salt to derive key encryption keys from passwords.

Password strings are treated as UTF-8 bytes.

```clojure

clj꞉com.example꞉> (pprint (encrypt "Attack at dawn" "Some strong password"))
{:cipher :aes-gcm,
:nonce [-99, 123, 66, -7, 55, -2, 100, -87, -9, -81, -121, 20],
:key-slots
[{:cipher :aes-gcm,
:nonce [65, -101, 97, 90, 74, -37, 47, 8, -70, -75, 0, -70],
:key-type :password,
:encrypted-key
[-61, 80, -6, -106, -14, 71, -31, 72, -42, 104, 15, 104, -65, 119,
-2, -20, -57, 7, -109, 47, -118, 120, -104, 5, -71, 118, -9, 86,
27, -128, -96, -45],
:kdf-params
{:kdf :pbkdf2-hmac-sha256,
:iterations 100000,
:salt
[-103, -25, 23, -67, -7, 69, 123, -108, 39, -24, 78, 26, -29, 60,
-2, 63]}}],
:ciphertext
[-14, 3, -11, 36, -111, 70, 98, -15, -36, 116, -37, -1, 78, -83, -34,
64, -92, -63, -21, -64, 109, -42, 104, 43, 41, 84, -64, 114, 90,
-69],
:flags #{:string}}


clj꞉com.example꞉> (def ciphertext
(encrypt "Attack at dawn"
"Some strong password"
:padded :json))
#'com.example/ciphertext ; formatted below
```
```json
{
"c": "aes-gcm",
"n": "b64:LJUha8j605ZJvF9n",
"ks": [
{
"c": "aes-gcm",
"n": "b64:uR_SwckAwBEmEWV4",
"kt": "p",
"ek": "b64:P6ld6SyDWwNu6MOzrZrGv9nXxtyJi3OwfqMxjQDRo7s",
"kp": {
"fn": "pb2hs256",
"it": 100000,
"sa": "b64:CQxR7kSMG2zZgSHSJpKcfw"
}
}
],
"ct": "b64:9F6EqB47WHaGJN9p0FYvj5si2sx5_eKFxwrX0oEWdPo",
"fl": [
"str",
"pad"
]
}
```

```clojure
clj꞉com.example꞉> (decrypt ciphertext "Some strong password")
"Attack at dawn"
```



### Binary key encryption

```clojure

clj꞉com.example꞉> (def plaintext (byte-array [1 2 3 4]))
#'com.example/plaintext

clj꞉com.example꞉> (def secret-key (rand-bytes 16)) ; or 24 or 32
#'com.example/secret-key

clj꞉com.example꞉> (def ciphertext (encrypt plaintext secret-key))
#'com.example/ciphertext

clj꞉com.example꞉> (pprint ciphertext)
{:cipher :aes-gcm,
:nonce [-91, -38, -58, 92, 76, -120, -18, -40, -10, -115, -66, -39],
:key-slots
[{:cipher :aes-gcm,
:nonce
[103, 109, -118, -88, -63, 21, -127, 119, -23, -92, 98, -126],
:key-type :binary,
:encrypted-key
[75, 5, 108, 45, 40, -37, -32, 75, 46, -97, 85, 120, 113, 117, -73,
-54, -94, 78, -124, 18, -107, 64, 10, -58, -128, 34, 72, 115, 59,
-123, 26, 50]}],
:ciphertext
[-121, 91, 80, 92, -50, -83, 73, -9, -87, -4, -122, -24, -51, -125,
57, -20, 89, 48, 6, -57]}

clj꞉com.example꞉> (vec (decrypt ciphertext secret-key))
[1 2 3 4]
```

### Multi-key encryption

Because `uniformity` uses a random data encryption key with slots for key
encryption keys, a number of keys can be used for the same ciphertext,
as opposed to encrypting the plaintext multiple times for different copies.

In the following example, we encrypt a piece of data both with a password,
and with a backup key (perhaps read from a file) in case the password is forgotten.

```clojure

clj꞉com.example꞉> (def backup-key
(hex-decode "9fa510b0ccc70f84c700189ec4f34bf4"))
#'com.example/backup-key

clj꞉com.example꞉> (def password "jmkPRiqVk3fgLZivqm"))
#'com.example/password

clj꞉com.example꞉> (def ciphertext
(encrypt "multi-key test"
[backup-key password]))
#'com.example/ciphertext



;; either key works to decrypt

clj꞉com.example꞉> (decrypt ciphertext "jmkPRiqVk3fgLZivqm")
"multi-key test"

clj꞉com.example꞉> (decrypt ciphertext
(hex-decode
"9fa510b0ccc70f84c700189ec4f34bf4"))
"multi-key test"
```
32 changes: 32 additions & 0 deletions doc/crypto/gcm_nonce_note.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# A Note on GCM Nonces

`uniformity` uses AES128-GCM with randomly-generated 96 bit nonces.
According to NIST guidelines, this is sufficient to use a given key up to
roughly 13 billion times.

I **speculate** that:
1) This limit is non-applicable to most use cases of `uniformity`.
2) This limit is entirely non-applicable due to the design of `uniformity`.

Please keep in mind that I am not a cryptographer for the following points.

There are three instances in which AES-GCM is used:

### Protecting the user-supplied plaintext
A unique data encryption key + nonce is generated for each encryption
operation,meaning this given key + nonce combo can only happen once.

### Using a password-generated key
A KDF-generated key should be generated with a unique salt each time,
again resulting in a technically-unique key for each operation.
This is up to the user, however, and we should encourage best practice
for this in the `uniformity` documentation.

### Using a (potentially long-lived) key encryption key.
The nonce limit is due to the potential of using the same key + nonce
on the same plaintext.
However, a key encryption key is exclusively used to protect the unique,
per-operation data encryption keys mentioned in the first point.
Since the chance of generating non-unique 128-bit value from a quality
CSPRNG is statistically zero, a repeated nonce would not be used
with a repeated plaintext DEK.
21 changes: 21 additions & 0 deletions doc/crypto/low_level_usage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Low-level Symmetric Encryption Usage

```clojure
(ns com.example
(:require [uniformity.crypto :refer [aes-gcm-encrypt
aes-gcm-decrypt]]
[uniformity.random :refer [rand-bytes]]
[uniformity.util :refer [str->utf8
utf8->str]]))

(def aes-key (rand-bytes (/ 128 8)))
(def gcm-nonce (rand-bytes (/ 96 8)))
(def plaintext (str->utf8 "Hello world"))

(def ciphertext
(aes-gcm-encrypt plaintext aes-key gcm-nonce))

(def decrypted
(utf8->str
(aes-gcm-decrypt ciphertext aes-key gcm-nonce)))
```
35 changes: 35 additions & 0 deletions doc/util.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,38 @@ clj꞉uniformity.util꞉> 
(into [] (base64-decode "FND_t6BzewM")))
true
```



# JSON

These functions are under-developed and should not be relied on.

```clojure
clj꞉uniformity.util꞉> (def some-struct {:foo :bar, :baz ["a" :b 3]})
#'uniformity.util/some-struct

clj꞉uniformity.util꞉> (def some-json (json-encode some-struct))
#'uniformity.util/some-json

clj꞉uniformity.util꞉> some-json
"[{\"foo\":\"bar\",\"baz\":[\"a\",\"b\",3]}]"

clj꞉uniformity.util꞉> (json-decode some-json)
[{"foo" "bar", "baz" ["a" "b" 3]}]
```



# UTF-8

```clojure
clj꞉uniformity.util꞉> (def test-utf8 (str->utf8 "Hello world 😎"))
#'uniformity.util/test-utf8

clj꞉uniformity.util꞉> (vec test-utf8)
[72 101 108 108 111 32 119 111 114 108 100 32 -16 -97 -104 -114]

clj꞉uniformity.util꞉> (utf8->str test-utf8)
"Hello world 😎"
```
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"dependencies": {
"asmcrypto.js": "^2.3.2",
"shadow-cljs": "^2.15.2"
}
}
3 changes: 2 additions & 1 deletion project.clj
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
:license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0"
:url "https://www.eclipse.org/legal/epl-2.0/"}
:dependencies [[org.clojure/clojure "1.10.1"]
[commons-codec/commons-codec "1.15"]]
[commons-codec/commons-codec "1.15"]
[org.clojure/data.json "2.4.0"]]
:repl-options {:init-ns uniformity.random})
Loading

0 comments on commit 0f52600

Please sign in to comment.