Skip to content

Commit 5720182

Browse files
committed
0.2.10: tighten error handling, add edge-case & interop tests
- added more tests for base64 decoding errors, stream frame limits, and runtime crossovers (CLI and browser), streawming and schemes - Implemented `zeroizeString()` for passphrase memory wiping - Consolidates CLI output-path validation logic into a reusable helper
1 parent 890f126 commit 5720182

19 files changed

Lines changed: 300 additions & 135 deletions

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ node_modules/
44
build/
55
dist/bin/cryptit
66
*.tmp
7-
scan.md
7+
scan*.md

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@mqxym/cryptit",
3-
"version": "0.2.9",
3+
"version": "0.2.10",
44
"description": "Modern, cross-platform AES-GCM 256 / XChaCha20Poly1305 + Argon2-id encryption for files and text.",
55
"type": "module",
66
"keywords": [

packages/browser-runtime/__tests__/browser.spec.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,16 @@ import { createCryptit } from '../src/index.js';
1010
describe('browser-runtime facade', () => {
1111
const crypt = createCryptit({ chunkSize: 1024 });
1212

13+
it('encrypts & decrypts in a browser context', async () => {
14+
const cipher = await crypt.encryptText('Foo', 'pw');
15+
const plain = await crypt.decryptText(cipher, 'pw');
16+
expect(plain).toBe('Foo');
17+
});
18+
});
19+
20+
describe('browser-runtime scheme 1', () => {
21+
const crypt = createCryptit({ chunkSize: 1024, scheme: 1 });
22+
1323
it('encrypts & decrypts in a browser context', async () => {
1424
const cipher = await crypt.encryptText('Foo', 'pw');
1525
const plain = await crypt.decryptText(cipher, 'pw');

packages/core/__tests__/cross.runtime.spec.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,11 @@ describe('cryptit CLI ↔ browser-runtime', () => {
2828
const plain = await crypt.decryptText(cipher, 'pw');
2929
expect(plain).toBe('interop');
3030
});
31+
32+
it('browser encrypt → `decrypt-text` (CLI)', async () => {
33+
const crypt = new Cryptit(browserProvider as CryptoProvider);
34+
const cipher = await crypt.encryptText('cli-roundtrip', 'pw');
35+
const plain = await runCli(['decrypt-text', cipher, '--pass', 'pw']);
36+
expect(plain).toBe('cli-roundtrip');
37+
});
3138
});

packages/core/__tests__/cryptit.file.spec.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,19 @@ const randomBlob = (bytes: number) =>
77
describe('Cryptit file helpers', () => {
88
const crypt = new Cryptit(nodeProvider);
99

10-
it('round-trips a 2 MiB Blob loss-lessly', async () => {
10+
it('round-trips a 2 MiB Blob loss-lessly', async () => {
11+
const plain = randomBlob(2_097_152); // 2 MiB
12+
const enc = await crypt.encryptFile(plain, 'hunter2');
13+
const dec = await crypt.decryptFile(enc, 'hunter2');
14+
15+
expect(await dec.arrayBuffer()).toEqual(await plain.arrayBuffer());
16+
});
17+
});
18+
19+
describe('Cryptit file helpers | Scheme 1', () => {
20+
const crypt = new Cryptit(nodeProvider, {scheme: 1} );
21+
22+
it('round-trips a 2 MiB Blob loss-lessly', async () => {
1123
const plain = randomBlob(2_097_152); // 2 MiB
1224
const enc = await crypt.encryptFile(plain, 'hunter2');
1325
const dec = await crypt.decryptFile(enc, 'hunter2');
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Cryptit } from '../src/index.js';
2+
import { nodeProvider } from '../../node-runtime/src/provider.js';
3+
4+
describe('Cryptit.headerDecode / isEncrypted helpers', () => {
5+
const crypt = new Cryptit(nodeProvider, { chunkSize: 1024 });
6+
7+
it('extracts meta-data from a Base64 payload', async () => {
8+
const cipher = await crypt.encryptText('meta-probe', 'pw');
9+
const meta = await Cryptit.headerDecode(cipher);
10+
11+
expect(meta).toMatchObject({
12+
scheme : crypt.getScheme(),
13+
difficulty: crypt.getDifficulty(),
14+
});
15+
expect(meta.saltLength).toBeGreaterThan(0);
16+
});
17+
18+
it('detects & decodes header embedded in a Blob', async () => {
19+
const blob = await crypt.encryptFile(new Blob([Uint8Array.of(1, 2)]), 'pw');
20+
expect(await Cryptit.isEncrypted(blob)).toBe(true);
21+
22+
const meta = await Cryptit.headerDecode(blob);
23+
expect(meta.scheme).toBe(crypt.getScheme());
24+
expect(meta.saltLength).toBeGreaterThan(0);
25+
});
26+
});

packages/core/__tests__/cryptit.stream.spec.ts

Lines changed: 0 additions & 48 deletions
This file was deleted.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { Cryptit } from '../src/index.js';
2+
import { nodeProvider } from '../../node-runtime/src/provider.js';
3+
4+
// jest.setTimeout(20_000);
5+
6+
/* ------------------------------------------------------------------ */
7+
/* Helper: collect full stream into a single Uint8Array */
8+
/* ------------------------------------------------------------------ */
9+
async function collect(rs: ReadableStream<Uint8Array>) {
10+
const rd = rs.getReader();
11+
const out: Uint8Array[] = [];
12+
for (;;) {
13+
const { done, value } = await rd.read();
14+
if (done) break;
15+
out.push(value);
16+
}
17+
return Uint8Array.from(out.flatMap(c => [...c]));
18+
}
19+
20+
/* ------------------------------------------------------------------ */
21+
/* Tests */
22+
/* ------------------------------------------------------------------ */
23+
describe('Cryptit streaming encrypt ↔ decrypt pipeline', () => {
24+
25+
const crypt = new Cryptit(nodeProvider, {
26+
chunkSize : 16_384,
27+
difficulty: 'low',
28+
scheme: 1,
29+
});
30+
31+
it('pipes through encrypt→decrypt streams with 65 kB input', async () => {
32+
const plain = crypto.getRandomValues(new Uint8Array(65_000));
33+
34+
/* —— encrypt —— */
35+
const { header, writable, readable } = await crypt.createEncryptionStream('pw');
36+
const w = writable.getWriter();
37+
w.write(plain.slice(0, 32_000)); // each write ≤ 65 536 B
38+
w.write(plain.slice(32_000));
39+
w.close();
40+
41+
const body = await collect(readable);
42+
const cipher = new Uint8Array(header.length + body.length);
43+
cipher.set(header);
44+
cipher.set(body, header.length);
45+
46+
/* —— decrypt —— */
47+
const decStream = await crypt.createDecryptionStream('pw');
48+
const rsPlain = new ReadableStream<Uint8Array>({
49+
start(c) { c.enqueue(cipher); c.close(); },
50+
}).pipeThrough(decStream);
51+
52+
const roundtrip = await collect(rsPlain);
53+
expect(roundtrip).toEqual(plain);
54+
});
55+
});

packages/core/__tests__/cryptit.text.spec.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,16 @@ describe('Cryptit text helpers - extra cases', () => {
5454
const plain = await browserCrypt.decryptText(cipher, 'pw');
5555
expect(plain).toBe('cross-ok');
5656
});
57+
58+
it('rejects **truncated ciphertext** with a meaningful error', async () => {
59+
const cipher = await crypt.encryptText('cut-off', 'pw');
60+
const damaged = cipher.slice(0, cipher.length - 10); // remove tail
61+
await expect(crypt.decryptText(damaged, 'pw')).rejects.toThrow();
62+
});
63+
64+
it('supports switching to **scheme 1** (XChaCha20-Poly1305)', async () => {
65+
crypt.setScheme(1);
66+
const cipher = await crypt.encryptText('scheme-1', 'pw');
67+
expect(await crypt.decryptText(cipher, 'pw')).toBe('scheme-1');
68+
});
5769
});
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { DecryptTransform } from '../src/stream/DecryptTransform.js';
2+
import type { EncryptionAlgorithm } from '../src/types/index.js';
3+
import { DecryptionError } from '../src/errors/index.js';
4+
5+
/* Naïve “no-op” engine - just echoes data back */
6+
class NopEngine implements EncryptionAlgorithm {
7+
async encryptChunk(p: Uint8Array) { return p; }
8+
async decryptChunk(c: Uint8Array) { return c; }
9+
async setKey() {}
10+
}
11+
12+
async function collect(rs: ReadableStream<Uint8Array>) {
13+
const r = rs.getReader(); const parts: Uint8Array[] = [];
14+
for (;;) { const { done, value } = await r.read(); if (done) break; parts.push(value); }
15+
return Uint8Array.from(parts.flatMap(b => [...b]));
16+
}
17+
18+
describe('DecryptTransform frame-length guard-rails', () => {
19+
it('throws DecryptionError when declared frame length exceeds **2: chunkSize**', async () => {
20+
const engine = new NopEngine();
21+
const ts = new DecryptTransform(engine, 8).toTransformStream(); // ⇒ limit = 16 bytes
22+
23+
/* Craft an invalid frame: header = 20 bytes ( > 16 ) */
24+
const bogus = new Uint8Array(4 + 20);
25+
new DataView(bogus.buffer).setUint32(0, 20, false);
26+
27+
const rs = new ReadableStream({ start(c) { c.enqueue(bogus); c.close(); } });
28+
29+
await expect(collect(rs.pipeThrough(ts))).rejects.toThrow(DecryptionError);
30+
});
31+
});

0 commit comments

Comments
 (0)