1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106 | (ns ird.p2p.db.crypto-test
"The key schedule and the sealed record: labels, derivations, seal/open
round trips, and every way opening must fail silently."
(:require [clojure.string :as str]
[cljs.test :refer [deftest is testing]]
[cljs.core.async :refer [<!]]
[ird.p2p.crypto :as p2p-crypto]
[ird.p2p.db.crypto :as crypto]
[ird.p2p.db.errors :as err]
[ird.p2p.db.test-util :as tu :refer [deftest-async]]
[ird.p2p.impl.bytes :as bytes]))
(def secret "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8") ; 32 bytes, URL-safe base64
(def salt "test/v1")
(deftest labels
(is (= "db|chat" (crypto/key-info "chat")))
(is (= "db|chat|identity" (crypto/identity-info "chat")))
(is (= "db|chat|identity|sign" (crypto/signing-info "chat")))
(is (= "db|chat|manifest" (crypto/manifest-info "chat")))
(is (= "db1|chat|log" (crypto/log-aad "chat")))
(is (= 2 crypto/prefix))
(is (= (* 256 1024) crypto/max-payload-bytes)))
(deftest names
(is (crypto/valid-name? "chat"))
(is (crypto/valid-name? "room 7 — notes"))
(is (not (crypto/valid-name? "")))
(is (not (crypto/valid-name? " ")))
(is (not (crypto/valid-name? "a|b")) "the label separator is reserved")
(is (not (crypto/valid-name? (apply str (repeat 129 "x")))))
(is (not (crypto/valid-name? :chat)))
(is (thrown? :default (crypto/check-name "a|b"))))
(deftest-async derivations
(let [k1 (<! (crypto/db-key secret salt "chat"))
k2 (<! (crypto/db-key secret salt "chat"))]
(is (= k1 k2) "deterministic")
(is (= 43 (count k1)) "32 bytes as URL-safe base64")
(is (not= k1 (<! (crypto/db-key secret salt "other"))) "differs by name")
(is (not= k1 (<! (crypto/db-key secret "other/v1" "chat"))) "differs by salt")
(is (not= k1 (<! (crypto/db-key (p2p-crypto/random-secret) salt "chat"))) "differs by secret")
(is (not= k1 (<! (crypto/identity-seed secret salt "chat"))))
(is (not= (<! (crypto/identity-seed secret salt "chat")) (<! (crypto/signing-seed secret salt "chat"))))
(is (= (<! (p2p-crypto/hkdf secret salt "db|chat")) k1) "K_db is HKDF(\"db|<name>\") of the sibling's HKDF")
(let [mn (<! (crypto/manifest-name secret salt "chat"))]
(is (= 43 (count mn)) "the manifest name is an opaque 32-byte derivation")
(is (= mn (<! (p2p-crypto/hkdf secret salt "db|chat|manifest"))))
(is (not= mn k1)))))
(deftest-async context
(let [dc (<! (crypto/db-crypto {:secret secret :app-salt salt :name "chat"}))]
(is (= "db1|chat|log" (:aad dc)))
(is (= (<! (crypto/db-key secret salt "chat")) (:key dc)))
(is (= (<! (crypto/manifest-name secret salt "chat")) (:manifest-name dc)))
(is (err/config-error? (<! (crypto/db-crypto {:secret nil :app-salt salt :name "chat"}))))
(is (err/config-error? (<! (crypto/db-crypto {:secret "AAECAwQFBgcICQoL" :app-salt salt :name "chat"}))) "12 bytes: too short")
(is (err/config-error? (<! (crypto/db-crypto {:secret "short" :app-salt salt :name "chat"}))))
(is (err/config-error? (<! (crypto/db-crypto {:secret secret :app-salt nil :name "chat"}))))
(is (err/config-error? (<! (crypto/db-crypto {:secret secret :app-salt salt :name "a|b"}))))))
(deftest serialize-is-ordered-json
(is (= "{\"b\":1,\"a\":\"x\"}" (bytes/b64->utf8 (crypto/serialize {:b 1 :a "x"}))))
(is (= "{\"text\":\"salut — ăîșț\",\"ok\":true,\"n\":[1,2.5,null]}"
(bytes/b64->utf8 (crypto/serialize {:text "salut — ăîșț" :ok true :n [1 2.5 nil]})))))
(deftest-async seal-and-open
(let [dc (<! (crypto/db-crypto {:secret secret :app-salt salt :name "chat"}))
other (<! (crypto/db-crypto {:secret secret :app-salt salt :name "other"}))
payload {:text "salut" :n 1 :ok true :nested {:a [1 2] :b nil}}
env (<! (crypto/seal dc payload))
env2 (<! (crypto/seal dc payload))]
(testing "shape: 0x02 ‖ iv(12) ‖ ct ‖ tag(16), standard base64"
(is (bytes/valid-b64? env))
(is (= 2 (bytes/byte-at env 0)))
(is (= (+ 1 12 (bytes/byte-length (crypto/serialize payload)) 16) (bytes/byte-length env)))
(is (not= env env2) "a fresh random IV per record")
(is (not= (bytes/slice-b64 env 1 13) (bytes/slice-b64 env2 1 13))))
(testing "round trip"
(is (= payload (<! (crypto/open dc env))))
(is (= payload (<! (crypto/open dc env2)))))
(testing "a fixed IV reproduces the same ciphertext (vectors only)"
(let [iv (bytes/hex->b64 "000102030405060708090a0b")]
(is (= (<! (crypto/seal dc payload iv)) (<! (crypto/seal dc payload iv))))))
(testing "every failure is nil"
(is (nil? (<! (crypto/open other env))) "another database's key and AAD")
(is (nil? (<! (crypto/open (assoc dc :aad (crypto/log-aad "other")) env))) "AAD mismatch alone")
(is (nil? (<! (crypto/open (assoc dc :key (<! (crypto/db-key secret salt "other"))) env))) "key mismatch alone")
(let [b (bytes/b64->bytes env)
flipped (bytes/bytes->b64 (update b 20 #(bit-xor % 1)))]
(is (nil? (<! (crypto/open dc flipped))) "one ciphertext bit flipped"))
(is (nil? (<! (crypto/open dc (bytes/concat-b64 (bytes/bytes->b64 [1]) (bytes/slice-b64 env 1))))) "wrong prefix")
(is (nil? (<! (crypto/open dc (bytes/slice-b64 env 0 20)))) "truncated")
(is (nil? (<! (crypto/open dc "not base64!"))))
(is (nil? (<! (crypto/open dc nil))))
(is (nil? (<! (crypto/open dc 42)))))
(testing "a plaintext that is not a JSON object is dropped"
(let [iv (p2p-crypto/random-iv)
ct (<! (p2p-crypto/aes-seal (:key dc) iv (:aad dc) (bytes/utf8->b64 "[1,2]")))
env-arr (bytes/concat-b64 (bytes/bytes->b64 [2]) iv ct)]
(is (nil? (<! (crypto/open dc env-arr))))))
(testing "limits and shapes"
(is (err/payload-too-large? (<! (crypto/seal dc {:blob (apply str (repeat (* 300 1024) "x"))}))))
(is (= {:limit (* 256 1024)} (select-keys (ex-data (<! (crypto/seal dc {:blob (apply str (repeat (* 300 1024) "x"))}))) [:limit])))
(is (err/config-error? (<! (crypto/seal dc "not a map"))))
(is (err/config-error? (<! (crypto/seal dc [1 2])))))))
|