@pioneer-dynamics/flashview-crypto 1.0.1

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.
Files changed (2) hide show
  1. package/package.json +30 -0
  2. package/src/index.js +145 -0
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@pioneer-dynamics/flashview-crypto",
3
+ "publishConfig": {
4
+ "access": "public"
5
+ },
6
+ "version": "1.0.1",
7
+ "description": "Isomorphic encryption package for FlashView — browser and Node.js",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/pioneer-dynamics/FlashView",
11
+ "directory": "tools/flashview-crypto"
12
+ },
13
+ "type": "module",
14
+ "exports": {
15
+ ".": "./src/index.js"
16
+ },
17
+ "files": [
18
+ "src/",
19
+ "README.md"
20
+ ],
21
+ "engines": {
22
+ "node": ">=20.0.0"
23
+ },
24
+ "scripts": {
25
+ "test": "node --test tests/*.test.js"
26
+ },
27
+ "dependencies": {
28
+ "random-words": "^2.0.0"
29
+ }
30
+ }
package/src/index.js ADDED
@@ -0,0 +1,145 @@
1
+ import { generate } from 'random-words';
2
+
3
+ const PBKDF2_ITERATIONS = 64000;
4
+ const KEY_LENGTH_BITS = 256;
5
+ const IV_LENGTH = 12; // 96-bit IV for AES-GCM
6
+ const SALT_LENGTH = 8; // 8 bytes = 16 hex chars
7
+
8
+ /**
9
+ * Generate an 8-word hyphenated passphrase.
10
+ *
11
+ * @returns {string}
12
+ */
13
+ export function generatePassphrase() {
14
+ return generate({ exactly: 8, join: '-' });
15
+ }
16
+
17
+ /**
18
+ * Derive an AES-256-GCM key from a passphrase and salt using PBKDF2-SHA-512.
19
+ *
20
+ * @param {string} passphrase
21
+ * @param {Uint8Array} salt
22
+ * @param {string[]} keyUsages
23
+ * @returns {Promise<CryptoKey>}
24
+ */
25
+ async function deriveKey(passphrase, salt, keyUsages) {
26
+ const passphraseKey = await globalThis.crypto.subtle.importKey(
27
+ 'raw',
28
+ new TextEncoder().encode(passphrase),
29
+ 'PBKDF2',
30
+ false,
31
+ ['deriveKey']
32
+ );
33
+
34
+ return globalThis.crypto.subtle.deriveKey(
35
+ { name: 'PBKDF2', salt, iterations: PBKDF2_ITERATIONS, hash: 'SHA-512' },
36
+ passphraseKey,
37
+ { name: 'AES-GCM', length: KEY_LENGTH_BITS },
38
+ false,
39
+ keyUsages
40
+ );
41
+ }
42
+
43
+ /**
44
+ * Convert a Uint8Array to a base64 string (isomorphic — works in browser and Node.js 18+).
45
+ *
46
+ * @param {Uint8Array} bytes
47
+ * @returns {string}
48
+ */
49
+ function uint8ArrayToBase64(bytes) {
50
+ let binary = '';
51
+ for (let i = 0; i < bytes.length; i++) {
52
+ binary += String.fromCharCode(bytes[i]);
53
+ }
54
+ return btoa(binary);
55
+ }
56
+
57
+ /**
58
+ * Convert a base64 string to a Uint8Array (isomorphic — works in browser and Node.js 18+).
59
+ *
60
+ * @param {string} base64
61
+ * @returns {Uint8Array}
62
+ */
63
+ function base64ToUint8Array(base64) {
64
+ const binary = atob(base64);
65
+ const bytes = new Uint8Array(binary.length);
66
+ for (let i = 0; i < binary.length; i++) {
67
+ bytes[i] = binary.charCodeAt(i);
68
+ }
69
+ return bytes;
70
+ }
71
+
72
+ /**
73
+ * Encrypt a plaintext message using AES-256-GCM with PBKDF2-SHA-512 key derivation.
74
+ *
75
+ * Output format: hex(8-byte salt) + base64(12-byte IV + encrypted data + 16-byte auth tag)
76
+ * This format is byte-identical to the existing browser (OpenCrypto) and CLI implementations.
77
+ *
78
+ * @param {string} message
79
+ * @param {string|null} passphrase - Auto-generated if null
80
+ * @returns {Promise<{ passphrase: string, secret: string }>}
81
+ */
82
+ export async function encryptMessage(message, passphrase = null) {
83
+ if (!passphrase) {
84
+ passphrase = generatePassphrase();
85
+ }
86
+
87
+ const salt = new Uint8Array(SALT_LENGTH);
88
+ globalThis.crypto.getRandomValues(salt);
89
+
90
+ const iv = new Uint8Array(IV_LENGTH);
91
+ globalThis.crypto.getRandomValues(iv);
92
+
93
+ const key = await deriveKey(passphrase, salt, ['encrypt']);
94
+
95
+ const ciphertext = await globalThis.crypto.subtle.encrypt(
96
+ { name: 'AES-GCM', iv },
97
+ key,
98
+ new TextEncoder().encode(message)
99
+ );
100
+
101
+ // Web Crypto AES-GCM appends the 16-byte auth tag at the end of ciphertext.
102
+ // Combine: IV (12 bytes) + ciphertext+authTag
103
+ const combined = new Uint8Array(IV_LENGTH + ciphertext.byteLength);
104
+ combined.set(iv);
105
+ combined.set(new Uint8Array(ciphertext), IV_LENGTH);
106
+
107
+ const saltHex = Array.from(salt).map(b => b.toString(16).padStart(2, '0')).join('');
108
+ const secret = saltHex + uint8ArrayToBase64(combined);
109
+
110
+ return { passphrase, secret };
111
+ }
112
+
113
+ /**
114
+ * Decrypt a ciphertext string using AES-256-GCM with PBKDF2-SHA-512 key derivation.
115
+ *
116
+ * Accepts ciphertexts produced by this package, the browser OpenCrypto implementation,
117
+ * or the Node.js CLI implementation — all use the same format.
118
+ *
119
+ * @param {string} ciphertextString - hex(salt) + base64(IV + encrypted + authTag)
120
+ * @param {string} passphrase
121
+ * @returns {Promise<string>}
122
+ */
123
+ export async function decryptMessage(ciphertextString, passphrase) {
124
+ const saltHex = ciphertextString.substring(0, 16);
125
+ const salt = new Uint8Array(SALT_LENGTH);
126
+ for (let i = 0; i < SALT_LENGTH; i++) {
127
+ salt[i] = parseInt(saltHex.substr(i * 2, 2), 16);
128
+ }
129
+
130
+ const combined = base64ToUint8Array(ciphertextString.substring(16));
131
+
132
+ // combined = IV (12 bytes) + encrypted data + auth tag (16 bytes)
133
+ const iv = combined.slice(0, IV_LENGTH);
134
+ const ciphertext = combined.slice(IV_LENGTH); // encrypted + authTag
135
+
136
+ const key = await deriveKey(passphrase, salt, ['decrypt']);
137
+
138
+ const decrypted = await globalThis.crypto.subtle.decrypt(
139
+ { name: 'AES-GCM', iv },
140
+ key,
141
+ ciphertext
142
+ );
143
+
144
+ return new TextDecoder().decode(decrypted);
145
+ }