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 +17 -1
- package/dist/index.d.ts +17 -1
- package/dist/index.js +87 -0
- package/dist/index.mjs +85 -0
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|