voidlogue-crypto 2.0.2 → 2.0.3

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/README.md CHANGED
@@ -66,6 +66,8 @@ const codename = generateCodename(4); // e.g. "correct-horse-battery-staple"
66
66
  | Method | Description |
67
67
  |---|---|
68
68
  | `hex(input)` | SHA-256 of input string → 64-char hex |
69
+ | `relationshipHash(emailA, emailB)` | Pure relationship identifier. Commutative. |
70
+ | `isInitiator(myEmail, theirEmail)` | Deterministic tie-breaker for UI deadlocks |
69
71
  | `roomId(emailA, emailB, codename)` | Derives opaque room hash. Commutative. |
70
72
  | `validateCodename(codename)` | Returns `{ valid, reason? }` |
71
73
  | `deriveKey(codename, roomHash)` | PBKDF2 → AES-256-GCM key (non-extractable) |
@@ -138,7 +140,8 @@ console.log(EFF_WORDLIST.length); // 7776
138
140
  ```
139
141
  Room hash:
140
142
  hA, hB = SHA-256(email.toLowerCase().trim())
141
- roomHash = SHA-256(sort([hA,hB]).join(":") + ":" + codename + ":" + APP_SALT)
143
+ relHash = SHA-256(sort([hA,hB]).join(":") + ":" + APP_SALT + ":relationship")
144
+ roomHash = SHA-256(relHash + ":" + codename + ":" + APP_SALT)
142
145
 
143
146
  Conversation key:
144
147
  key = PBKDF2(codename, salt=roomHash, iters=600_000, hash=SHA-256) → AES-256-GCM
package/SECURITY.md CHANGED
@@ -10,7 +10,7 @@ against the actual code running in the browser.
10
10
 
11
11
  ### Claim 1: "We cannot read your Conversation messages"
12
12
 
13
- **Code that proves it:** `src/voidshield.ts` — `roomId()`, `deriveKey()`, `encrypt()`
13
+ **Code that proves it:** `src/voidshield.ts` — `relationshipHash()`, `roomId()`, `deriveKey()`, `encrypt()`
14
14
 
15
15
  Room hashes are derived entirely client-side from the pair of email addresses
16
16
  and the shared codename. The algorithm:
@@ -18,7 +18,8 @@ and the shared codename. The algorithm:
18
18
  ```
19
19
  hA = SHA-256(emailA.toLowerCase().trim())
20
20
  hB = SHA-256(emailB.toLowerCase().trim())
21
- roomHash = SHA-256(sort([hA, hB]).join(":") + ":" + codename + ":" + APP_SALT)
21
+ relHash = SHA-256(sort([hA, hB]).join(":") + ":" + APP_SALT + ":relationship")
22
+ roomHash = SHA-256(relHash + ":" + codename + ":" + APP_SALT)
22
23
  ```
23
24
 
24
25
  The server receives only `roomHash` — an opaque 64-character hex string. It
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "voidlogue-crypto",
3
- "version": "2.0.2",
3
+ "version": "2.0.3",
4
4
  "description": "Open-source client-side cryptographic primitives for Voidlogue — published for independent audit and verification of privacy claims.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -45,14 +45,14 @@
45
45
  "node": ">=18.0.0"
46
46
  },
47
47
  "devDependencies": {
48
- "@eslint/js": "^8.0.0",
49
- "@types/node": "^20.0.0",
50
- "eslint": "^8.0.0",
48
+ "@eslint/js": "^10.0.1",
49
+ "@types/node": "^25.5.2",
50
+ "eslint": "^10.2.0",
51
51
  "expect": "^30.3.0",
52
- "fast-check": "^3.23.2",
52
+ "fast-check": "^4.6.0",
53
53
  "prettier": "^3.0.0",
54
54
  "tsx": "^4.21.0",
55
- "typescript": "^5.0.0",
55
+ "typescript": "^6.0.2",
56
56
  "typescript-eslint": "^8.58.0"
57
57
  },
58
58
  "scripts": {
package/src/voidshield.ts CHANGED
@@ -65,6 +65,8 @@ export interface KyberKeyPair {
65
65
 
66
66
  export interface VoidShieldAPI {
67
67
  hex(input: string): Promise<string>;
68
+ relationshipHash(emailA: string, emailB: string): Promise<string>;
69
+ isInitiator(myEmail: string, theirEmail: string): Promise<boolean>;
68
70
  roomId(emailA: string, emailB: string, codename: string): Promise<string>;
69
71
  validateCodename(codename: string): CodenameValidation;
70
72
  deriveKey(codename: string, roomHash: string): Promise<CryptoKey>;
@@ -210,16 +212,27 @@ export const VoidShield: VoidShieldAPI = {
210
212
 
211
213
  // ── Conversation ─────────────────────────────────────────────────────────
212
214
 
215
+ async relationshipHash(emailA: string, emailB: string): Promise<string> {
216
+ const [hA, hB] = await Promise.all([
217
+ this.hex(emailA.toLowerCase().trim()),
218
+ this.hex(emailB.toLowerCase().trim()),
219
+ ]);
220
+ return this.hex(`${[hA, hB].sort().join(':')}:${APP_SALT}:relationship`);
221
+ },
222
+
223
+ async isInitiator(myEmail: string, theirEmail: string): Promise<boolean> {
224
+ const hashMe = await this.hex(myEmail.toLowerCase().trim());
225
+ const hashThem = await this.hex(theirEmail.toLowerCase().trim());
226
+ return hashMe < hashThem;
227
+ },
228
+
213
229
  async roomId(
214
230
  emailA: string,
215
231
  emailB: string,
216
232
  codename: string
217
233
  ): Promise<string> {
218
- const [hA, hB] = await Promise.all([
219
- this.hex(emailA.toLowerCase().trim()),
220
- this.hex(emailB.toLowerCase().trim()),
221
- ]);
222
- return this.hex(`${[hA, hB].sort().join(':')}:${codename}:${APP_SALT}`);
234
+ const relHash = await this.relationshipHash(emailA, emailB);
235
+ return this.hex(`${relHash}:${codename}:${APP_SALT}`);
223
236
  },
224
237
 
225
238
  validateCodename(codename: string): CodenameValidation {