Any document store, with its documents encrypted at rest.
A server or a synced folder then holds only ciphertext, and nothing else changes. Names and versions pass through, and bodies are sealed on the way in and opened on the way out.
A body is sealed with AES-256-GCM under a key derived from the passphrase by PBKDF2 with HMAC-SHA256. Each document is a small EDN envelope with the salt, IV and iteration count that open it, so there's no key file to lose:
{:encrypted 1 :cipher "AES-256-GCM" :kdf "PBKDF2-HMAC-SHA256"
:iterations 600000 :salt "…" :iv "…" :data "…"}
A body that isn't an envelope comes back as it is, so you can turn on encryption over a folder that already holds plain documents. A passphrase that doesn't open a document makes that call of the store fail with ::locked.
Since WebCrypto is asynchronous, the functions here and those of the store return promises in ClojureScript. Each platform opens the envelopes of the other.
Any document store, with its documents encrypted at rest.
A server or a synced folder then holds only ciphertext, and nothing else
changes. Names and versions pass through, and bodies are sealed on the
way in and opened on the way out.
A body is sealed with AES-256-GCM under a key derived from the passphrase
by PBKDF2 with HMAC-SHA256. Each document is a small EDN envelope with
the salt, IV and iteration count that open it, so there's no key file to
lose:
{:encrypted 1 :cipher "AES-256-GCM" :kdf "PBKDF2-HMAC-SHA256"
:iterations 600000 :salt "…" :iv "…" :data "…"}
A body that isn't an envelope comes back as it is, so you can turn on
encryption over a folder that already holds plain documents. A
passphrase that doesn't open a document makes that call of the store
fail with ::locked.
Since WebCrypto is asynchronous, the functions here and those of the
store return promises in ClojureScript. Each platform opens the
envelopes of the other.(decrypt s passphrase)The body in the envelope string s under passphrase, or s itself
when it isn't an envelope, in ClojureScript as a promise.
A passphrase that doesn't open it throws ::locked, or in ClojureScript, rejects the promise with it.
The body in the envelope string `s` under `passphrase`, or `s` itself when it isn't an envelope, in ClojureScript as a promise. A passphrase that doesn't open it throws ::locked, or in ClojureScript, rejects the promise with it.
The PBKDF2 iterations of a key, unless the options give others.
It's what the OWASP Password Storage Cheat Sheet recommends for HMAC-SHA256 as of September 2026. On a phone, a key takes about half a second, once per salt.
The PBKDF2 iterations of a key, unless the options give others. It's what the OWASP Password Storage Cheat Sheet recommends for HMAC-SHA256 as of September 2026. On a phone, a key takes about half a second, once per salt.
(encrypt body passphrase)(encrypt body
passphrase
{:keys [iterations] :or {iterations default-iterations} :as opts})The envelope string for body under passphrase, with a fresh salt and
IV and the :iterations of opts, in ClojureScript as a promise. The
iterations default to default-iterations.
The envelope string for `body` under `passphrase`, with a fresh salt and IV and the :iterations of `opts`, in ClojureScript as a promise. The iterations default to default-iterations.
(envelope s)The envelope in the string s, or nil for anything else, a plain
document included.
An envelope is a map of :encrypted, :cipher, :kdf, :iterations, :salt, :iv and :data. The last three are in base64, and the iterations are at least one and at most max-iterations.
The envelope in the string `s`, or nil for anything else, a plain document included. An envelope is a map of :encrypted, :cipher, :kdf, :iterations, :salt, :iv and :data. The last three are in base64, and the iterations are at least one and at most max-iterations.
The most PBKDF2 iterations an envelope can ask for, ten times the default. A document that asks for more isn't taken for an envelope, since deriving its key would hold up a device for minutes.
The most PBKDF2 iterations an envelope can ask for, ten times the default. A document that asks for more isn't taken for an envelope, since deriving its key would hold up a device for minutes.
(store inner passphrase)(store inner
passphrase
{:keys [iterations except]
:or {iterations default-iterations except #{}}
:as opts})The store inner with its documents encrypted at rest under
passphrase, with the opts below.
The passphrase is a string or a function of no arguments that returns
one. The string can be a hidden text of
dk.simongray.wary-fetch.secret. The opts are:
A key is derived once for each salt and iteration count, and cached.
Each store seals with a salt of its own, and opens any salt that a
document has. It writes, lists and deletes only when inner does.
When the passphrase is a function, a document that the cached keys don't open empties the cache. The document is then tried once more with what the function gives, so that a corrected passphrase is used from then on.
The store `inner` with its documents encrypted at rest under `passphrase`, with the `opts` below. The `passphrase` is a string or a function of no arguments that returns one. The string can be a hidden text of dk.simongray.wary-fetch.secret. The `opts` are: - :iterations, the PBKDF2 iterations of the key, default-iterations by default - :except, the names of documents to leave in the clear, e.g. an extra document of a backend that other apps should read A key is derived once for each salt and iteration count, and cached. Each store seals with a salt of its own, and opens any salt that a document has. It writes, lists and deletes only when `inner` does. When the passphrase is a function, a document that the cached keys don't open empties the cache. The document is then tried once more with what the function gives, so that a corrected passphrase is used from then on.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |