voidlogue-crypto 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,57 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Voidlogue
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ ---
24
+
25
+ ## License rationale
26
+
27
+ This package is published under the MIT license for the following reasons:
28
+
29
+ 1. **Auditability over restriction.** The purpose of open-sourcing this package
30
+ is to allow independent verification of Voidlogue's privacy claims. A
31
+ permissive license removes barriers to inspection and review.
32
+
33
+ 2. **Cryptographic primitives benefit from wide scrutiny.** Security research
34
+ is best served by making cryptographic code as accessible as possible.
35
+ Restrictive licenses discourage security researchers.
36
+
37
+ 3. **The word list is already public.** The EFF long wordlist included in this
38
+ package is published by the Electronic Frontier Foundation under an open
39
+ license. There is no proprietary component to protect here.
40
+
41
+ 4. **This package alone cannot replicate Voidlogue.** The client-side crypto
42
+ layer requires a server (Phoenix/Elixir backend, PostgreSQL, Oban jobs,
43
+ push infrastructure) which is not included here. Permissive licensing of
44
+ this package does not expose the platform's competitive differentiators.
45
+
46
+ ## What the MIT license means for you
47
+
48
+ - You can read, inspect, fork, and use this code for any purpose
49
+ - You can use it in commercial products
50
+ - You must include the copyright notice in any copy or substantial portion
51
+ - There is no warranty — use at your own risk
52
+
53
+ ## What this license does NOT cover
54
+
55
+ The Voidlogue platform server code, the Revelation product, the admin
56
+ infrastructure, and the marketing pages are proprietary and are NOT covered
57
+ by this license. This license applies only to the files in this repository.
package/README.md ADDED
@@ -0,0 +1,215 @@
1
+ # voidlogue-crypto
2
+
3
+ **Open-source cryptographic layer for Voidlogue — published for independent audit and verification.**
4
+
5
+ [![npm version](https://img.shields.io/npm/v/voidlogue-crypto.svg)](https://www.npmjs.com/package/voidlogue-crypto)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-teal.svg)](./LICENSE)
7
+
8
+ This package contains exactly the code running in the browser at [voidlogue.com](https://voidlogue.com).
9
+ It is published so that Voidlogue's privacy claims can be independently verified.
10
+
11
+ ---
12
+
13
+ ## What this proves
14
+
15
+ | Claim | Code |
16
+ |---|---|
17
+ | "We cannot read your messages" | `VoidShield.roomId()`, `VoidShield.deriveKey()`, `VoidShield.encrypt()` |
18
+ | "We cannot read your Revelations" | `VoidShield.deriveRevelationKey()`, `VoidShield.encryptMedia()` |
19
+ | "Your saved shortcuts are encrypted locally" | `Vault.save()`, `Vault.load()` |
20
+
21
+ See [SECURITY.md](./SECURITY.md) for a full technical explanation of each claim.
22
+
23
+ ---
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ npm install voidlogue-crypto
29
+ ```
30
+
31
+ ---
32
+
33
+ ## Usage
34
+
35
+ ```javascript
36
+ import { VoidShield, generateCodename } from "voidlogue-crypto";
37
+
38
+ // Derive a room hash (server never sees emails or codename)
39
+ const roomHash = await VoidShield.roomId(
40
+ "alice@example.com",
41
+ "bob@example.com",
42
+ "iron-falcon-sky"
43
+ );
44
+
45
+ // Derive encryption key
46
+ const key = await VoidShield.deriveKey("iron-falcon-sky", roomHash);
47
+
48
+ // Encrypt a message
49
+ const { ciphertextB64, ivB64 } = await VoidShield.encrypt("hello", key);
50
+
51
+ // Decrypt (throws if key is wrong — AES-GCM authentication)
52
+ const plaintext = await VoidShield.decrypt(ciphertextB64, ivB64, key);
53
+
54
+ // Generate a random codename from the EFF wordlist
55
+ const codename = generateCodename(4); // e.g. "correct-horse-battery-staple"
56
+ ```
57
+
58
+ ---
59
+
60
+ ## API Reference
61
+
62
+ ### `VoidShield`
63
+
64
+ #### Conversation
65
+
66
+ | Method | Description |
67
+ |---|---|
68
+ | `hex(input)` | SHA-256 of input string → 64-char hex |
69
+ | `roomId(emailA, emailB, codename)` | Derives opaque room hash. Commutative. |
70
+ | `validateCodename(codename)` | Returns `{ valid, reason? }` |
71
+ | `deriveKey(codename, roomHash)` | PBKDF2 → AES-256-GCM key (non-extractable) |
72
+ | `encrypt(plaintext, key)` | AES-256-GCM → `{ ciphertextB64, ivB64 }` |
73
+ | `decrypt(ciphertextB64, ivB64, key)` | AES-256-GCM → plaintext string |
74
+ | `senderHash(uuid, roomHash)` | Room-scoped sender identifier |
75
+
76
+ #### Revelation
77
+
78
+ | Method | Description |
79
+ |---|---|
80
+ | `deriveRevelationKey(senderEmail, recipientEmail, fieldValues[])` | Key from emails + security fields |
81
+ | `deriveRevelationKeyFromHashes(senderHash, recipientHash, fieldValues[])` | Key from pre-computed hashes |
82
+ | `hashFieldValue(value)` | Normalise + SHA-256 for server storage |
83
+ | `encryptMedia(file, key)` | Chunked file encryption → chunk array |
84
+ | `decryptMediaStream(chunks, key)` | Async generator → ArrayBuffer per chunk |
85
+
86
+ #### Utilities
87
+
88
+ | Method | Description |
89
+ |---|---|
90
+ | `secureRandom(max)` | Uniform random int in `[0, max)` — rejection sampling |
91
+
92
+ ### `generateCodename(wordCount?)`
93
+
94
+ Generates a hyphen-separated passphrase from the EFF long wordlist.
95
+
96
+ - Default: 3 words (~38 bits of entropy)
97
+ - Range: 2–8 words, clamped
98
+ - 4 words ≈ 51 bits, 5 words ≈ 64 bits, 6 words ≈ 77 bits
99
+
100
+ ```javascript
101
+ generateCodename() // "iron-falcon-sky"
102
+ generateCodename(5) // "correct-horse-battery-staple-moon"
103
+ ```
104
+
105
+ ### `Vault`
106
+
107
+ PIN-based local encryption for saved conversation credentials.
108
+
109
+ ```javascript
110
+ import { Vault } from "voidlogue-crypto";
111
+
112
+ // Save email + codename encrypted with PIN
113
+ await Vault.save(roomHash, email, codename, "123456", "Alice — work");
114
+
115
+ // Load (returns {email, codename} or {error: "wrong_pin"|"locked"|"not_found"})
116
+ const result = await Vault.load(roomHash, "123456");
117
+
118
+ // List saved conversations (no sensitive data)
119
+ const list = Vault.list(); // [{roomHash, hint (encrypted), savedAt}]
120
+
121
+ // Wipe all (panic clear)
122
+ Vault.wipeAll();
123
+ ```
124
+
125
+ ### `EFF_WORDLIST`
126
+
127
+ The complete EFF long wordlist — 7776 words, frozen array.
128
+
129
+ ```javascript
130
+ import { EFF_WORDLIST } from "voidlogue-crypto";
131
+ console.log(EFF_WORDLIST.length); // 7776
132
+ ```
133
+
134
+ ---
135
+
136
+ ## Cryptographic algorithm summary
137
+
138
+ ```
139
+ Room hash:
140
+ hA, hB = SHA-256(email.toLowerCase().trim())
141
+ roomHash = SHA-256(sort([hA,hB]).join(":") + ":" + codename + ":" + APP_SALT)
142
+
143
+ Conversation key:
144
+ key = PBKDF2(codename, salt=roomHash, iters=100_000, hash=SHA-256) → AES-256-GCM
145
+
146
+ Revelation key:
147
+ hS, hR = SHA-256(email.toLowerCase().trim())
148
+ fh[] = SHA-256(normalise(fieldValue))
149
+ input = sort([hS,hR]).join(":") + ":" + fh.join(":")
150
+ key = PBKDF2(input, salt="voidlogue-revelation-v1", iters=100_000) → AES-256-GCM
151
+
152
+ Vault PIN key:
153
+ key = PBKDF2(PIN, random_16B_salt, iters=100_000, hash=SHA-256) → AES-256-GCM
154
+
155
+ All encryption: AES-256-GCM with random 96-bit IV per operation
156
+ All randomness: crypto.getRandomValues() with rejection sampling
157
+ ```
158
+
159
+ No third-party cryptographic libraries. All operations use the Web Crypto API.
160
+
161
+ ---
162
+
163
+ ## Running the tests
164
+
165
+ ```bash
166
+ npm install
167
+ npm test
168
+ ```
169
+
170
+ Tests cover:
171
+ - SHA-256 against known vectors
172
+ - `roomId` commutativity, determinism, isolation
173
+ - Encrypt/decrypt round-trip and tamper detection
174
+ - Revelation key derivation cross-compatibility
175
+ - `secureRandom` uniformity and bounds
176
+ - Media chunk encryption and decryption
177
+ - `generateCodename` wordlist coverage
178
+ - `EFF_WORDLIST` integrity (7776 entries, no duplicates, frozen)
179
+ - Security invariants (ciphertext contains no plaintext, hashes contain no inputs)
180
+
181
+ ---
182
+
183
+ ## Verifying the deployed code
184
+
185
+ The code at [voidlogue.com](https://voidlogue.com) uses this package directly.
186
+ To verify:
187
+
188
+ 1. Open voidlogue.com in your browser
189
+ 2. DevTools → Sources → search for `VoidShield` or `roomId`
190
+ 3. Compare the implementation against this repository
191
+
192
+ ---
193
+
194
+ ## What is NOT in this package
195
+
196
+ - Server code (Phoenix/Elixir backend)
197
+ - Database schema or queries
198
+ - The Revelation product server logic
199
+ - Payment or subscription code
200
+ - Admin infrastructure
201
+ - Anything that runs outside the browser
202
+
203
+ ---
204
+
205
+ ## License
206
+
207
+ MIT — see [LICENSE](./LICENSE) for details and rationale.
208
+
209
+ ## Security disclosure
210
+
211
+ See [SECURITY.md](./SECURITY.md) — email security@voidlogue.com for vulnerabilities.
212
+
213
+ ---
214
+
215
+ *Part of [Voidlogue](https://voidlogue.com) — Said once. Gone forever.*
package/SECURITY.md ADDED
@@ -0,0 +1,154 @@
1
+ # Security Policy
2
+
3
+ ## What this package proves
4
+
5
+ This package is the open-source cryptographic layer for Voidlogue. It is
6
+ published so that the following privacy claims can be independently verified
7
+ against the actual code running in the browser.
8
+
9
+ ---
10
+
11
+ ### Claim 1: "We cannot read your Conversation messages"
12
+
13
+ **Code that proves it:** `src/voidshield.js` — `roomId()`, `deriveKey()`, `encrypt()`
14
+
15
+ Room hashes are derived entirely client-side from the pair of email addresses
16
+ and the shared codename. The algorithm:
17
+
18
+ ```
19
+ hA = SHA-256(emailA.toLowerCase().trim())
20
+ hB = SHA-256(emailB.toLowerCase().trim())
21
+ roomHash = SHA-256(sort([hA, hB]).join(":") + ":" + codename + ":" + APP_SALT)
22
+ ```
23
+
24
+ The server receives only `roomHash` — an opaque 64-character hex string. It
25
+ cannot reverse this to learn the email addresses or the codename.
26
+
27
+ The encryption key is derived from the codename:
28
+
29
+ ```
30
+ key = PBKDF2(codename, salt=roomHash, iterations=100_000, hash=SHA-256)
31
+ → AES-256-GCM key (non-extractable)
32
+ ```
33
+
34
+ The codename is never sent to the server. The server stores only
35
+ `AES-256-GCM(plaintext, key, random_IV)` — ciphertext it cannot decrypt
36
+ because it never held the key material.
37
+
38
+ ---
39
+
40
+ ### Claim 2: "We cannot read your Revelations"
41
+
42
+ **Code that proves it:** `src/voidshield.js` — `deriveRevelationKey()`,
43
+ `deriveRevelationKeyFromHashes()`, `encryptMedia()`
44
+
45
+ Revelation content is encrypted before upload. The key is derived from:
46
+
47
+ ```
48
+ hS = SHA-256(senderEmail.toLowerCase().trim())
49
+ hR = SHA-256(recipientEmail.toLowerCase().trim())
50
+ fh[] = SHA-256(normalise(fieldValue)) for each security field
51
+
52
+ input = sort([hS, hR]).join(":") + ":" + fh.join(":")
53
+ key = PBKDF2(input, salt="voidlogue-revelation-v1", iterations=100_000)
54
+ → AES-256-GCM key
55
+ ```
56
+
57
+ The security field values (e.g. the recipient's date of birth, first name)
58
+ are never sent to the server. The server stores only the hashes of field
59
+ values for access control purposes — and never the values themselves. The
60
+ decryption key cannot be derived without the raw field values which only the
61
+ recipient holds.
62
+
63
+ Delivery does not require decryption. The server relays encrypted bytes it
64
+ cannot read.
65
+
66
+ ---
67
+
68
+ ### Claim 3: "Your saved conversation shortcuts are encrypted locally"
69
+
70
+ **Code that proves it:** `src/vault.js`
71
+
72
+ The Vault encrypts the user's email and codename on-device before storing
73
+ them in `localStorage`:
74
+
75
+ ```
76
+ key = PBKDF2(PIN, random_salt, iterations=100_000) → AES-256-GCM key
77
+ blob = AES-256-GCM(JSON({email, codename}), key, random_IV)
78
+ ```
79
+
80
+ The PIN is never stored anywhere. The server is never involved. Even if
81
+ someone extracts the device's `localStorage` they receive AES-256-GCM
82
+ ciphertext that cannot be decrypted without the PIN.
83
+
84
+ ---
85
+
86
+ ## Cryptographic primitive choices
87
+
88
+ | Primitive | Algorithm | Rationale |
89
+ |---|---|---|
90
+ | Symmetric encryption | AES-256-GCM | NIST-approved; provides authenticated encryption (tamper detection) |
91
+ | Key derivation | PBKDF2, SHA-256, 100k iterations | Standardised; makes brute-force computationally expensive |
92
+ | Hashing | SHA-256 | Collision-resistant; output is 256 bits |
93
+ | Randomness | `crypto.getRandomValues` with rejection sampling | Cryptographically secure; rejection sampling eliminates modular bias |
94
+
95
+ No third-party cryptographic libraries are used. All operations use the
96
+ Web Crypto API built into the browser.
97
+
98
+ ---
99
+
100
+ ## What this package does NOT prove
101
+
102
+ - That the server does not log data it should not log (requires server audit)
103
+ - That the server deletes messages as described (requires server audit)
104
+ - That the N=1 constraint is enforced server-side (requires server audit)
105
+ - That the server does not store session data beyond what is stated
106
+
107
+ These claims require auditing the server code, which is not included here.
108
+ We will answer specific questions about the server implementation directly.
109
+
110
+ ---
111
+
112
+ ## Threat model
113
+
114
+ ### Protects against
115
+
116
+ - Platform reading message content (server never holds keys)
117
+ - Server breach exposing message content (only ciphertext stored)
118
+ - Legal compulsion to produce message content (server has nothing to produce)
119
+ - Person with physical device access seeing conversation content
120
+
121
+ ### Does NOT protect against
122
+
123
+ - Government subpoena for metadata (who talked to whom, when)
124
+ - Nation-state network surveillance
125
+ - The counterparty sharing decrypted content
126
+ - Screen photography
127
+ - A PIN brute-force attack on a stolen device (mitigated by lockout)
128
+ - Compromise of the user's Google account (authentication layer)
129
+
130
+ ---
131
+
132
+ ## Reporting vulnerabilities
133
+
134
+ **Please do not open a public issue for security vulnerabilities.**
135
+
136
+ Email: security@voidlogue.com
137
+
138
+ We will acknowledge within 48 hours and aim to resolve critical issues within
139
+ 7 days. We will credit researchers in release notes unless anonymity is
140
+ requested.
141
+
142
+ ## Scope
143
+
144
+ In scope:
145
+ - Cryptographic implementation errors in this package
146
+ - Key derivation weaknesses
147
+ - Randomness or bias issues in `secureRandom()`
148
+ - Any input that allows recovery of plaintext without the correct key
149
+
150
+ Out of scope:
151
+ - Social engineering attacks
152
+ - Physical device access attacks
153
+ - Issues in the server-side code (not in this repo)
154
+ - Theoretical weaknesses in AES-256-GCM or PBKDF2 as standardised algorithms
package/index.js ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * voidlogue-crypto — Voidlogue Client-Side Cryptography
3
+ *
4
+ * Open-source cryptographic layer for independent audit and verification.
5
+ * Published so that the privacy claims made at voidlogue.com can be verified
6
+ * against the actual code running in the browser.
7
+ *
8
+ * @module voidlogue-crypto
9
+ */
10
+
11
+ export { VoidShield, generateCodename } from "./src/voidshield.js";
12
+ export { Vault, LabelCipher } from "./src/vault.js";
13
+ export { EFF_WORDLIST } from "./src/eff_wordlist.js";
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "voidlogue-crypto",
3
+ "version": "1.0.0",
4
+ "description": "Open-source client-side cryptographic primitives for Voidlogue — published for independent audit and verification of privacy claims.",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": "./index.js",
9
+ "./voidshield": "./src/voidshield.js",
10
+ "./vault": "./src/vault.js",
11
+ "./eff_wordlist": "./src/eff_wordlist.js"
12
+ },
13
+ "files": [
14
+ "src/",
15
+ "index.js",
16
+ "README.md",
17
+ "SECURITY.md",
18
+ "LICENSE"
19
+ ],
20
+ "keywords": [
21
+ "encryption",
22
+ "privacy",
23
+ "ephemeral",
24
+ "messaging",
25
+ "aes-gcm",
26
+ "pbkdf2",
27
+ "web-crypto",
28
+ "voidlogue",
29
+ "e2e",
30
+ "zero-knowledge"
31
+ ],
32
+ "license": "MIT",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/voidlogue/voidlogue-crypto.git"
36
+ },
37
+ "homepage": "https://voidlogue.com",
38
+ "bugs": {
39
+ "url": "https://github.com/voidlogue/voidlogue-crypto/issues",
40
+ "email": "security@voidlogue.com"
41
+ },
42
+ "engines": {
43
+ "node": ">=18.0.0"
44
+ },
45
+ "devDependencies": {
46
+ "vitest": "^1.0.0"
47
+ },
48
+ "scripts": {
49
+ "test": "vitest run",
50
+ "test:watch": "vitest"
51
+ }
52
+ }