quantumsure-qstore 0.1.0 → 0.2.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/dist/index.d.mts CHANGED
@@ -20,4 +20,20 @@ declare function qstoreEncrypt(plaintext: string, publicKeyB64: string): Promise
20
20
  */
21
21
  declare function qstoreDecrypt(encryptedBlob: string, privateKeyB64: string): Promise<string>;
22
22
 
23
- export { qstoreDecrypt, qstoreEncrypt };
23
+ /**
24
+ * Encrypt file bytes for a recipient ML-KEM-1024 public key (base64).
25
+ *
26
+ * Bundle format (.qsecure):
27
+ * version (2 bytes: 1,0)
28
+ * kemLen (2 bytes, big-endian)
29
+ * kemCiphertext
30
+ * nonce (24 bytes)
31
+ * encryptedFile (XChaCha20-Poly1305)
32
+ */
33
+ declare function encryptFileBytes(fileBytes: Uint8Array, publicKeyB64: string): Promise<Uint8Array>;
34
+ /**
35
+ * Decrypt a .qsecure bundle with the recipient ML-KEM-1024 private key (base64).
36
+ */
37
+ declare function decryptFileBytes(bundle: Uint8Array, privateKeyB64: string): Promise<Uint8Array>;
38
+
39
+ export { decryptFileBytes, encryptFileBytes, qstoreDecrypt, qstoreEncrypt };
package/dist/index.d.ts CHANGED
@@ -20,4 +20,20 @@ declare function qstoreEncrypt(plaintext: string, publicKeyB64: string): Promise
20
20
  */
21
21
  declare function qstoreDecrypt(encryptedBlob: string, privateKeyB64: string): Promise<string>;
22
22
 
23
- export { qstoreDecrypt, qstoreEncrypt };
23
+ /**
24
+ * Encrypt file bytes for a recipient ML-KEM-1024 public key (base64).
25
+ *
26
+ * Bundle format (.qsecure):
27
+ * version (2 bytes: 1,0)
28
+ * kemLen (2 bytes, big-endian)
29
+ * kemCiphertext
30
+ * nonce (24 bytes)
31
+ * encryptedFile (XChaCha20-Poly1305)
32
+ */
33
+ declare function encryptFileBytes(fileBytes: Uint8Array, publicKeyB64: string): Promise<Uint8Array>;
34
+ /**
35
+ * Decrypt a .qsecure bundle with the recipient ML-KEM-1024 private key (base64).
36
+ */
37
+ declare function decryptFileBytes(bundle: Uint8Array, privateKeyB64: string): Promise<Uint8Array>;
38
+
39
+ export { decryptFileBytes, encryptFileBytes, qstoreDecrypt, qstoreEncrypt };
package/dist/index.js CHANGED
@@ -20,6 +20,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ decryptFileBytes: () => decryptFileBytes,
24
+ encryptFileBytes: () => encryptFileBytes,
23
25
  qstoreDecrypt: () => qstoreDecrypt,
24
26
  qstoreEncrypt: () => qstoreEncrypt
25
27
  });
@@ -117,8 +119,93 @@ async function qstoreDecrypt(encryptedBlob, privateKeyB64) {
117
119
  const decrypted = chacha.decrypt(encrypted);
118
120
  return new TextDecoder().decode(decrypted);
119
121
  }
122
+
123
+ // src/file.ts
124
+ var import_mlkem3 = require("mlkem");
125
+ var import_chacha3 = require("@noble/ciphers/chacha.js");
126
+ var import_buffer2 = require("buffer");
127
+ function randomBytes2(length) {
128
+ const out = new Uint8Array(length);
129
+ crypto.getRandomValues(out);
130
+ return out;
131
+ }
132
+ function fromBase642(b64) {
133
+ return new Uint8Array(import_buffer2.Buffer.from(b64, "base64"));
134
+ }
135
+ function concatBytes(...parts) {
136
+ const total = parts.reduce((n, p) => n + p.length, 0);
137
+ const out = new Uint8Array(total);
138
+ let offset = 0;
139
+ for (const part of parts) {
140
+ out.set(part, offset);
141
+ offset += part.length;
142
+ }
143
+ return out;
144
+ }
145
+ async function encryptFileBytes(fileBytes, publicKeyB64) {
146
+ if (!(fileBytes instanceof Uint8Array) || fileBytes.length === 0) {
147
+ throw new Error("encryptFileBytes: fileBytes must be a non-empty Uint8Array");
148
+ }
149
+ if (!publicKeyB64) {
150
+ throw new Error("encryptFileBytes: publicKeyB64 is required");
151
+ }
152
+ const publicKey = fromBase642(publicKeyB64);
153
+ const kem = new import_mlkem3.MlKem1024();
154
+ const [kemCiphertext, fileKey] = await kem.encap(publicKey);
155
+ const nonce = randomBytes2(24);
156
+ const encryptedFile = (0, import_chacha3.xchacha20poly1305)(fileKey, nonce).encrypt(fileBytes);
157
+ const version = new Uint8Array([1, 0]);
158
+ const kemLenBytes = new Uint8Array([
159
+ kemCiphertext.length >> 8 & 255,
160
+ kemCiphertext.length & 255
161
+ ]);
162
+ return concatBytes(
163
+ version,
164
+ kemLenBytes,
165
+ kemCiphertext,
166
+ nonce,
167
+ encryptedFile
168
+ );
169
+ }
170
+ async function decryptFileBytes(bundle, privateKeyB64) {
171
+ if (!(bundle instanceof Uint8Array)) {
172
+ throw new Error("decryptFileBytes: bundle must be a Uint8Array");
173
+ }
174
+ if (!privateKeyB64) {
175
+ throw new Error("decryptFileBytes: privateKeyB64 is required");
176
+ }
177
+ if (bundle.length < 2 + 2 + 24) {
178
+ throw new Error("decryptFileBytes: file is too short to be valid");
179
+ }
180
+ const version = bundle.slice(0, 2);
181
+ if (version[0] !== 1 || version[1] !== 0) {
182
+ throw new Error(
183
+ `decryptFileBytes: unsupported file version ${version[0]}.${version[1]}`
184
+ );
185
+ }
186
+ const kemLen = bundle[2] << 8 | bundle[3];
187
+ if (kemLen < 1568 || kemLen > 2e3) {
188
+ throw new Error("decryptFileBytes: invalid KEM ciphertext length");
189
+ }
190
+ const offset = 4;
191
+ if (bundle.length < offset + kemLen + 24) {
192
+ throw new Error("decryptFileBytes: truncated bundle");
193
+ }
194
+ const kemCiphertext = bundle.slice(offset, offset + kemLen);
195
+ const nonce = bundle.slice(offset + kemLen, offset + kemLen + 24);
196
+ const encryptedFile = bundle.slice(offset + kemLen + 24);
197
+ if (encryptedFile.length === 0) {
198
+ throw new Error("decryptFileBytes: no encrypted data found");
199
+ }
200
+ const privateKey = fromBase642(privateKeyB64);
201
+ const kem = new import_mlkem3.MlKem1024();
202
+ const fileKey = await kem.decap(kemCiphertext, privateKey);
203
+ return (0, import_chacha3.xchacha20poly1305)(fileKey, nonce).decrypt(encryptedFile);
204
+ }
120
205
  // Annotate the CommonJS export names for ESM import in node:
121
206
  0 && (module.exports = {
207
+ decryptFileBytes,
208
+ encryptFileBytes,
122
209
  qstoreDecrypt,
123
210
  qstoreEncrypt
124
211
  });
package/dist/index.mjs CHANGED
@@ -90,7 +90,92 @@ async function qstoreDecrypt(encryptedBlob, privateKeyB64) {
90
90
  const decrypted = chacha.decrypt(encrypted);
91
91
  return new TextDecoder().decode(decrypted);
92
92
  }
93
+
94
+ // src/file.ts
95
+ import { MlKem1024 as MlKem10243 } from "mlkem";
96
+ import { xchacha20poly1305 } from "@noble/ciphers/chacha.js";
97
+ import { Buffer as Buffer2 } from "buffer";
98
+ function randomBytes2(length) {
99
+ const out = new Uint8Array(length);
100
+ crypto.getRandomValues(out);
101
+ return out;
102
+ }
103
+ function fromBase642(b64) {
104
+ return new Uint8Array(Buffer2.from(b64, "base64"));
105
+ }
106
+ function concatBytes(...parts) {
107
+ const total = parts.reduce((n, p) => n + p.length, 0);
108
+ const out = new Uint8Array(total);
109
+ let offset = 0;
110
+ for (const part of parts) {
111
+ out.set(part, offset);
112
+ offset += part.length;
113
+ }
114
+ return out;
115
+ }
116
+ async function encryptFileBytes(fileBytes, publicKeyB64) {
117
+ if (!(fileBytes instanceof Uint8Array) || fileBytes.length === 0) {
118
+ throw new Error("encryptFileBytes: fileBytes must be a non-empty Uint8Array");
119
+ }
120
+ if (!publicKeyB64) {
121
+ throw new Error("encryptFileBytes: publicKeyB64 is required");
122
+ }
123
+ const publicKey = fromBase642(publicKeyB64);
124
+ const kem = new MlKem10243();
125
+ const [kemCiphertext, fileKey] = await kem.encap(publicKey);
126
+ const nonce = randomBytes2(24);
127
+ const encryptedFile = xchacha20poly1305(fileKey, nonce).encrypt(fileBytes);
128
+ const version = new Uint8Array([1, 0]);
129
+ const kemLenBytes = new Uint8Array([
130
+ kemCiphertext.length >> 8 & 255,
131
+ kemCiphertext.length & 255
132
+ ]);
133
+ return concatBytes(
134
+ version,
135
+ kemLenBytes,
136
+ kemCiphertext,
137
+ nonce,
138
+ encryptedFile
139
+ );
140
+ }
141
+ async function decryptFileBytes(bundle, privateKeyB64) {
142
+ if (!(bundle instanceof Uint8Array)) {
143
+ throw new Error("decryptFileBytes: bundle must be a Uint8Array");
144
+ }
145
+ if (!privateKeyB64) {
146
+ throw new Error("decryptFileBytes: privateKeyB64 is required");
147
+ }
148
+ if (bundle.length < 2 + 2 + 24) {
149
+ throw new Error("decryptFileBytes: file is too short to be valid");
150
+ }
151
+ const version = bundle.slice(0, 2);
152
+ if (version[0] !== 1 || version[1] !== 0) {
153
+ throw new Error(
154
+ `decryptFileBytes: unsupported file version ${version[0]}.${version[1]}`
155
+ );
156
+ }
157
+ const kemLen = bundle[2] << 8 | bundle[3];
158
+ if (kemLen < 1568 || kemLen > 2e3) {
159
+ throw new Error("decryptFileBytes: invalid KEM ciphertext length");
160
+ }
161
+ const offset = 4;
162
+ if (bundle.length < offset + kemLen + 24) {
163
+ throw new Error("decryptFileBytes: truncated bundle");
164
+ }
165
+ const kemCiphertext = bundle.slice(offset, offset + kemLen);
166
+ const nonce = bundle.slice(offset + kemLen, offset + kemLen + 24);
167
+ const encryptedFile = bundle.slice(offset + kemLen + 24);
168
+ if (encryptedFile.length === 0) {
169
+ throw new Error("decryptFileBytes: no encrypted data found");
170
+ }
171
+ const privateKey = fromBase642(privateKeyB64);
172
+ const kem = new MlKem10243();
173
+ const fileKey = await kem.decap(kemCiphertext, privateKey);
174
+ return xchacha20poly1305(fileKey, nonce).decrypt(encryptedFile);
175
+ }
93
176
  export {
177
+ decryptFileBytes,
178
+ encryptFileBytes,
94
179
  qstoreDecrypt,
95
180
  qstoreEncrypt
96
181
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quantumsure-qstore",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Quantum-resistant field-level encryption for client data. Encrypt before storage, decrypt on read — plaintext never leaves your environment.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",