Skip to content

Commit a5bc21e

Browse files
authored
Merge pull request #10 from devlux76/copilot/p0-a-crypto-helpers-implementation
2 parents 2176045 + 8e7cf04 commit a5bc21e

File tree

8 files changed

+393
-28
lines changed

8 files changed

+393
-28
lines changed

PLAN.md

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ This document tracks the implementation status of each major module in CORTEX. I
2323
| Core Types | ✅ Complete | `core/types.ts` | All entity interfaces defined (Page, Book, Volume, Shelf, Edge, MetroidNeighbor, storage interfaces) |
2424
| Model Profiles | ✅ Complete | `core/ModelProfile.ts`, `core/ModelDefaults.ts`, `core/ModelProfileResolver.ts`, `core/BuiltInModelProfiles.ts` | Source-of-truth for model-derived numerics; guard script enforces compliance |
2525
| Numeric Constants | ✅ Complete | `core/NumericConstants.ts` | Runtime constants (byte sizes, workgroup limits) centralized |
26-
| Crypto Helpers | ❌ Missing | `core/crypto/*` (planned) | Hash, sign, verify utilities not yet implemented |
26+
| Crypto Helpers | ✅ Complete | `core/crypto/hash.ts`, `core/crypto/sign.ts`, `core/crypto/verify.ts` | SHA-256 hashing; Ed25519 sign/verify; 26 tests passing |
2727

28-
**Foundation Status:** 3/4 complete (75%)
28+
**Foundation Status:** 4/4 complete (100%)
2929

3030
---
3131

@@ -148,7 +148,7 @@ This document tracks the implementation status of each major module in CORTEX. I
148148

149149
| Module | Status | Files | Notes |
150150
|--------|--------|-------|-------|
151-
| Unit Tests | ✅ Complete | `tests/*.test.ts`, `tests/**/*.test.ts` | 89 tests across 10 files; all passing |
151+
| Unit Tests | ✅ Complete | `tests/*.test.ts`, `tests/**/*.test.ts` | 115 tests across 13 files; all passing |
152152
| Persistence Tests | ✅ Complete | `tests/Persistence.test.ts` | Full storage layer coverage (OPFS, IndexedDB, Metroid neighbors) |
153153
| Model Tests | ✅ Complete | `tests/model/*.test.ts` | Profile resolution, defaults, routing policy |
154154
| Embedding Tests | ✅ Complete | `tests/embeddings/*.test.ts` | Provider resolver, runner, real/dummy backends |
@@ -180,7 +180,7 @@ This document tracks the implementation status of each major module in CORTEX. I
180180

181181
| Layer | Completion | Critical Gap |
182182
|-------|-----------|--------------|
183-
| Foundation | 75% | Crypto helpers |
183+
| Foundation | 100% | |
184184
| Storage | 100% ||
185185
| Vector Compute | 100% ||
186186
| Embedding | 83% | WebGL provider (low priority) |
@@ -203,14 +203,14 @@ This document tracks the implementation status of each major module in CORTEX. I
203203
- ✅ Generate real embeddings via Transformers.js
204204
- ✅ Resolve model profiles and derive routing policies
205205
- ✅ Run browser/Electron runtime harness
206-
- ✅ Pass 89 unit tests
206+
- ✅ Pass 115 unit tests
207+
- ✅ Hash text/binary content (SHA-256) and sign/verify Ed25519 signatures
207208

208209
## What Doesn't Work Today
209210

210211
-**Cannot ingest text** — No chunking or hierarchy builder
211212
-**Cannot query memories** — No ranking pipeline or TSP solver
212213
-**Cannot consolidate** — No Daydreamer loop
213-
-**Cannot sign/verify** — No crypto helpers
214214

215215
---
216216

@@ -220,10 +220,10 @@ This document tracks the implementation status of each major module in CORTEX. I
220220

221221
**Goal:** Enable ingest and retrieval for a single user session.
222222

223-
1. **Crypto Helpers** (`core/crypto/*`)
224-
- Implement SHA-256 hashing
225-
- Implement Ed25519 signing/verification
226-
- Test coverage
223+
1. **Crypto Helpers** (`core/crypto/*`)**Complete**
224+
- SHA-256 hashing for text and binary
225+
- Ed25519 signing/verification
226+
- 26 tests passing
227227

228228
2. **Text Chunking** (`hippocampus/Chunker.ts`)
229229
- Token-aware splitting respecting ModelProfile limits
@@ -336,10 +336,6 @@ This document tracks the implementation status of each major module in CORTEX. I
336336
**Impact:** Cannot retrieve memories.
337337
**Mitigation:** Phase 1 priority; flat ranking acceptable for v0.1.
338338

339-
### Blocker 3: No Crypto Helpers
340-
**Impact:** Cannot sign/verify pages (integrity risk).
341-
**Mitigation:** Phase 1 priority; required for trustworthy storage.
342-
343339
### Risk 1: TSP Complexity
344340
Open TSP is NP-hard; heuristic may be slow on large subgraphs.
345341
**Mitigation:** Bound subgraph size (<30 nodes); defer to Phase 2; use deterministic greedy heuristic.

TODO.md

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,30 +10,30 @@ This document contains a prioritized, actionable list of specific tasks required
1010

1111
These items **must** be completed to have a usable system. Without them, users cannot ingest or query memories.
1212

13-
### P0-A: Crypto Helpers (BLOCKS: all signed entity creation)
13+
### P0-A: Crypto Helpers (BLOCKS: all signed entity creation) ✅ COMPLETE
1414

1515
**Why:** Pages require `contentHash`, `vectorHash`, and `signature`. Cannot create valid pages without crypto.
1616

17-
- [ ] **P0-A1:** Implement `core/crypto/hash.ts`
17+
- [x] **P0-A1:** Implement `core/crypto/hash.ts`
1818
- SHA-256 for text content
1919
- SHA-256 for binary vector data
2020
- Test with known vectors
2121

22-
- [ ] **P0-A2:** Implement `core/crypto/sign.ts`
22+
- [x] **P0-A2:** Implement `core/crypto/sign.ts`
2323
- Ed25519 key pair generation
2424
- Sign canonical page representation
2525
- Test with example pages
2626

27-
- [ ] **P0-A3:** Implement `core/crypto/verify.ts`
27+
- [x] **P0-A3:** Implement `core/crypto/verify.ts`
2828
- Verify signature against public key
2929
- Test reject invalid signatures
3030

31-
- [ ] **P0-A4:** Add crypto test coverage
31+
- [x] **P0-A4:** Add crypto test coverage
3232
- `tests/crypto/hash.test.ts`
3333
- `tests/crypto/sign.test.ts`
3434
- `tests/crypto/verify.test.ts`
3535

36-
**Exit Criteria:** Can hash content, sign pages, verify signatures.
36+
**Exit Criteria:** Can hash content, sign pages, verify signatures. ✅ Met — 26 tests passing.
3737

3838
---
3939

@@ -479,7 +479,7 @@ These items improve quality, performance, and developer experience. Not blockers
479479

480480
| Phase | Items | Status | Blocking |
481481
|-------|-------|--------|----------|
482-
| v0.1 (Minimal Viable) | 17 tasks (P0-A through P0-E) | ❌ Not started | User cannot use system |
482+
| v0.1 (Minimal Viable) | 17 tasks (P0-A through P0-E) | 🟡 In Progress (P0-A complete) | User cannot use system |
483483
| v0.5 (Hierarchical + Coherent) | 13 tasks (P1-A through P1-F) | ❌ Not started | Blocked by v0.1 |
484484
| v1.0 (Background Consolidation) | 11 tasks (P2-A through P2-E) | ❌ Not started | Blocked by v0.5 |
485485
| Polish & Ship | 14 tasks (P3-A through P3-F) | ❌ Not started | Not blocking v1.0 |
@@ -492,13 +492,11 @@ These items improve quality, performance, and developer experience. Not blockers
492492

493493
If you're reading this and want to know "what do I work on right now?", here's the answer:
494494

495-
1. **P0-A1:** Implement `core/crypto/hash.ts` (SHA-256)
496-
2. **P0-A2:** Implement `core/crypto/sign.ts` (Ed25519)
497-
3. **P0-A3:** Implement `core/crypto/verify.ts`
498-
4. **P0-B1:** Implement `hippocampus/Chunker.ts`
499-
5. **P0-C1:** Implement `hippocampus/PageBuilder.ts`
500-
501-
Complete these 5 tasks and you'll unblock the entire ingest path. Then move to P0-D (query) to complete v0.1.
495+
1. **P0-B1:** Implement `hippocampus/Chunker.ts`
496+
2. **P0-C1:** Implement `hippocampus/PageBuilder.ts`
497+
3. **P0-C2:** Implement `hippocampus/Ingest.ts`
498+
4. **P0-D1:** Implement `cortex/Query.ts`
499+
5. **P0-E1:** Implement `tests/integration/IngestQuery.test.ts`
502500

503501
---
504502

core/crypto/hash.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import type { Hash } from "../types.js";
2+
3+
function bufferToHex(buffer: ArrayBuffer): string {
4+
return Array.from(new Uint8Array(buffer))
5+
.map(b => b.toString(16).padStart(2, "0"))
6+
.join("");
7+
}
8+
9+
/**
10+
* Returns the SHA-256 hex digest of a UTF-8 encoded text string.
11+
* Used to produce `contentHash` on Page entities.
12+
*/
13+
export async function hashText(content: string): Promise<Hash> {
14+
const encoded = new TextEncoder().encode(content);
15+
const buffer = await crypto.subtle.digest("SHA-256", encoded);
16+
return bufferToHex(buffer);
17+
}
18+
19+
/**
20+
* Returns the SHA-256 hex digest of raw binary data.
21+
* Used to produce `vectorHash` on Page entities.
22+
*/
23+
export async function hashBinary(data: BufferSource): Promise<Hash> {
24+
const buffer = await crypto.subtle.digest("SHA-256", data);
25+
return bufferToHex(buffer);
26+
}

core/crypto/sign.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import type { PublicKey, Signature } from "../types.js";
2+
3+
export interface KeyPair {
4+
/** JWK JSON string — safe to store and share. */
5+
publicKey: PublicKey;
6+
/** JWK JSON string — store securely; used to reconstruct `signingKey`. */
7+
privateKeyJwk: string;
8+
/** Runtime CryptoKey ready for immediate signing operations. */
9+
signingKey: CryptoKey;
10+
}
11+
12+
function bufferToBase64(buffer: ArrayBuffer): Signature {
13+
return btoa(String.fromCharCode(...new Uint8Array(buffer)));
14+
}
15+
16+
/**
17+
* Generates a new Ed25519 key pair.
18+
* Returns the public key as a JWK string, the private key as both a JWK
19+
* string (for secure storage) and a runtime `CryptoKey` (for signing).
20+
*/
21+
export async function generateKeyPair(): Promise<KeyPair> {
22+
const keyPair = await crypto.subtle.generateKey(
23+
{ name: "Ed25519" } as Algorithm,
24+
true,
25+
["sign", "verify"],
26+
) as CryptoKeyPair;
27+
28+
const [publicKeyJwk, privateKeyJwk] = await Promise.all([
29+
crypto.subtle.exportKey("jwk", keyPair.publicKey),
30+
crypto.subtle.exportKey("jwk", keyPair.privateKey),
31+
]);
32+
33+
return {
34+
publicKey: JSON.stringify(publicKeyJwk),
35+
privateKeyJwk: JSON.stringify(privateKeyJwk),
36+
signingKey: keyPair.privateKey,
37+
};
38+
}
39+
40+
/**
41+
* Imports a private key from its JWK JSON string for use in signing.
42+
* Call this when restoring a key pair from persistent storage.
43+
*/
44+
export async function importSigningKey(privateKeyJwk: string): Promise<CryptoKey> {
45+
const jwk = JSON.parse(privateKeyJwk) as JsonWebKey;
46+
return crypto.subtle.importKey(
47+
"jwk",
48+
jwk,
49+
{ name: "Ed25519" } as Algorithm,
50+
false,
51+
["sign"],
52+
);
53+
}
54+
55+
/**
56+
* Signs arbitrary data with an Ed25519 private key.
57+
* Returns a base64-encoded signature string.
58+
*
59+
* @param data - UTF-8 string or raw bytes to sign.
60+
* @param signingKey - CryptoKey from `generateKeyPair()` or `importSigningKey()`.
61+
*/
62+
export async function signData(
63+
data: string | ArrayBuffer,
64+
signingKey: CryptoKey,
65+
): Promise<Signature> {
66+
const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
67+
const signatureBuffer = await crypto.subtle.sign("Ed25519", signingKey, bytes);
68+
return bufferToBase64(signatureBuffer);
69+
}

core/crypto/verify.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import type { PublicKey, Signature } from "../types.js";
2+
3+
function base64ToBuffer(base64: Signature): ArrayBuffer | null {
4+
try {
5+
const binary = atob(base64);
6+
const bytes = new Uint8Array(binary.length);
7+
for (let i = 0; i < binary.length; i++) {
8+
bytes[i] = binary.charCodeAt(i);
9+
}
10+
return bytes.buffer;
11+
} catch {
12+
return null;
13+
}
14+
}
15+
16+
/**
17+
* Verifies an Ed25519 signature produced by `signData`.
18+
*
19+
* Returns `true` if `signature` is a valid Ed25519 signature over `data`
20+
* by the key encoded in `publicKey` (JWK JSON string).
21+
* Returns `false` for any signature mismatch or malformed signature.
22+
* Throws for structurally invalid public key (malformed JWK JSON).
23+
*
24+
* @param data - The original data that was signed (string or bytes).
25+
* @param signature - Base64-encoded signature from `signData()`.
26+
* @param publicKey - JWK JSON string from `KeyPair.publicKey`.
27+
*/
28+
export async function verifySignature(
29+
data: string | ArrayBuffer,
30+
signature: Signature,
31+
publicKey: PublicKey,
32+
): Promise<boolean> {
33+
const signatureBuffer = base64ToBuffer(signature);
34+
if (signatureBuffer === null) {
35+
return false;
36+
}
37+
38+
const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
39+
const publicKeyJwk = JSON.parse(publicKey) as JsonWebKey;
40+
41+
const cryptoKey = await crypto.subtle.importKey(
42+
"jwk",
43+
publicKeyJwk,
44+
{ name: "Ed25519" } as Algorithm,
45+
false,
46+
["verify"],
47+
);
48+
49+
return crypto.subtle.verify("Ed25519", cryptoKey, signatureBuffer, bytes);
50+
}

tests/crypto/hash.test.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { describe, expect, it } from "vitest";
2+
import { createHash } from "node:crypto";
3+
import { hashText, hashBinary } from "../../core/crypto/hash.js";
4+
5+
describe("hashText", () => {
6+
it("returns a 64-character hex string (SHA-256)", async () => {
7+
const result = await hashText("hello");
8+
expect(result).toHaveLength(64);
9+
expect(result).toMatch(/^[0-9a-f]+$/);
10+
});
11+
12+
it("cross-validates against node:crypto createHash for consistent output", async () => {
13+
const inputs = ["hello", "world", "cortex memory engine", "abc", ""];
14+
for (const input of inputs) {
15+
const ours = await hashText(input);
16+
const native = createHash("sha256").update(input).digest("hex");
17+
expect(ours).toBe(native);
18+
}
19+
});
20+
21+
it("matches known SHA-256 digest for the empty string", async () => {
22+
// SHA-256("") is a well-known NIST constant
23+
const result = await hashText("");
24+
expect(result).toBe("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
25+
});
26+
27+
it("produces different hashes for different inputs", async () => {
28+
const h1 = await hashText("hello");
29+
const h2 = await hashText("world");
30+
expect(h1).not.toBe(h2);
31+
});
32+
33+
it("is deterministic — same input yields identical hash", async () => {
34+
const content = "deterministic test input";
35+
const h1 = await hashText(content);
36+
const h2 = await hashText(content);
37+
expect(h1).toBe(h2);
38+
});
39+
});
40+
41+
describe("hashBinary", () => {
42+
it("returns a 64-character hex string (SHA-256)", async () => {
43+
const data = new Uint8Array([1, 2, 3, 4]);
44+
const result = await hashBinary(data);
45+
expect(result).toHaveLength(64);
46+
expect(result).toMatch(/^[0-9a-f]+$/);
47+
});
48+
49+
it("matches known SHA-256 digest for single zero byte", async () => {
50+
// SHA-256(0x00) = 6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d
51+
const data = new Uint8Array([0x00]);
52+
const result = await hashBinary(data);
53+
expect(result).toBe("6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d");
54+
});
55+
56+
it("accepts ArrayBuffer input", async () => {
57+
const data = new Uint8Array([0xde, 0xad, 0xbe, 0xef]).buffer;
58+
const result = await hashBinary(data);
59+
expect(result).toHaveLength(64);
60+
});
61+
62+
it("produces different hashes for different byte arrays", async () => {
63+
const h1 = await hashBinary(new Uint8Array([1, 2, 3]));
64+
const h2 = await hashBinary(new Uint8Array([4, 5, 6]));
65+
expect(h1).not.toBe(h2);
66+
});
67+
68+
it("is consistent with hashText for the same encoded content", async () => {
69+
const content = "consistent";
70+
const textHash = await hashText(content);
71+
const binaryHash = await hashBinary(new TextEncoder().encode(content));
72+
expect(textHash).toBe(binaryHash);
73+
});
74+
});

0 commit comments

Comments
 (0)