p2p-db / test / ird / p2p / db / crypto_test.cljs
726 words
  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])))))))

static mirror of HEAD · about · clone: git clone https://ird.cod-sursa.ro/p2p-db.git