@batalabs/virlow-crypto 3.97.0 → 3.98.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.
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The format for a file too large to hold as one ciphertext: v3.
3
+ *
4
+ * The file gets a random key of its own. That key, wrapped by the account's
5
+ * master key, is the header "v3:<ivB64>:<wrappedKeyB64>", which is all the
6
+ * row's encryptedData column holds. The content is cut into chunks of a fixed
7
+ * plaintext size and each is sealed on its own with AES-256-GCM, so a reader
8
+ * can fetch and check any chunk without the rest, and neither end ever holds
9
+ * the whole file in memory.
10
+ *
11
+ * A chunk is bound to its place: its nonce is its index, and its index and
12
+ * whether it is the last chunk are authenticated with it. A store that
13
+ * reorders chunks, drops some from the end or hands back one file's chunk as
14
+ * another's is caught by the tag. The index can be the whole nonce because
15
+ * the key is used for this one file and nothing else.
16
+ */
17
+ /** AES-GCM appends its authentication tag to each chunk. */
18
+ export declare const FILE_CHUNK_OVERHEAD = 16;
19
+ export declare function isV3Attachment(encryptedData: string): boolean;
20
+ /** A new file key and the header that carries it, wrapped by the master key. */
21
+ export declare function createFileKey(masterKey: CryptoKey): Promise<{
22
+ header: string;
23
+ fileKey: CryptoKey;
24
+ }>;
25
+ /** The file key out of a header. Fails (OperationError) under the wrong master key. */
26
+ export declare function openFileKey(masterKey: CryptoKey, header: string): Promise<CryptoKey>;
27
+ /** Seals chunk `index` (0-based). The result is the chunk plus FILE_CHUNK_OVERHEAD bytes. */
28
+ export declare function encryptFileChunk(fileKey: CryptoKey, index: number, isLast: boolean, plain: Uint8Array | ArrayBuffer): Promise<Uint8Array>;
29
+ /** Opens chunk `index`. Fails if it is not that chunk of that file, or was cut short. */
30
+ export declare function decryptFileChunk(fileKey: CryptoKey, index: number, isLast: boolean, sealed: Uint8Array | ArrayBuffer): Promise<Uint8Array>;
31
+ /** How many chunks a file of `size` bytes is cut into. */
32
+ export declare function fileChunkCount(size: number, chunkSize: number): number;
33
+ /** Where sealed chunk `index` sits in the stored object: [start, end), in bytes. */
34
+ export declare function sealedChunkRange(size: number, chunkSize: number, index: number): {
35
+ start: number;
36
+ end: number;
37
+ };
38
+ //# sourceMappingURL=chunked.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunked.d.ts","sourceRoot":"","sources":["../src/chunked.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;GAeG;AAEH,4DAA4D;AAC5D,eAAO,MAAM,mBAAmB,KAAK,CAAC;AAStC,wBAAgB,cAAc,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAE7D;AASD,gFAAgF;AAChF,wBAAsB,aAAa,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,SAAS,CAAA;CAAE,CAAC,CAgBzG;AAED,uFAAuF;AACvF,wBAAsB,WAAW,CAAC,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAkB1F;AAeD,6FAA6F;AAC7F,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,SAAS,EAClB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,OAAO,EACf,KAAK,EAAE,UAAU,GAAG,WAAW,GAC9B,OAAO,CAAC,UAAU,CAAC,CAErB;AAED,yFAAyF;AACzF,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,SAAS,EAClB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,UAAU,GAAG,WAAW,GAC/B,OAAO,CAAC,UAAU,CAAC,CAErB;AAED,0DAA0D;AAC1D,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAEtE;AAED,oFAAoF;AACpF,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,GACZ;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAKhC"}
@@ -0,0 +1,100 @@
1
+ import { base64ToBytes, bytesToBase64 } from './base64.js';
2
+ import { ENCRYPTION_CONFIG } from './constants.js';
3
+ /**
4
+ * The format for a file too large to hold as one ciphertext: v3.
5
+ *
6
+ * The file gets a random key of its own. That key, wrapped by the account's
7
+ * master key, is the header "v3:<ivB64>:<wrappedKeyB64>", which is all the
8
+ * row's encryptedData column holds. The content is cut into chunks of a fixed
9
+ * plaintext size and each is sealed on its own with AES-256-GCM, so a reader
10
+ * can fetch and check any chunk without the rest, and neither end ever holds
11
+ * the whole file in memory.
12
+ *
13
+ * A chunk is bound to its place: its nonce is its index, and its index and
14
+ * whether it is the last chunk are authenticated with it. A store that
15
+ * reorders chunks, drops some from the end or hands back one file's chunk as
16
+ * another's is caught by the tag. The index can be the whole nonce because
17
+ * the key is used for this one file and nothing else.
18
+ */
19
+ /** AES-GCM appends its authentication tag to each chunk. */
20
+ export const FILE_CHUNK_OVERHEAD = 16;
21
+ /** The size GCM is specified for. Nothing random goes in it, so there is no
22
+ * reason for the longer IV the rest of the formats use. */
23
+ const CHUNK_NONCE_BYTES = 12;
24
+ const HEADER_PREFIX = 'v3:';
25
+ const FILE_KEY_BYTES = 32;
26
+ const AAD_LABEL = new TextEncoder().encode('virlow-file-v3');
27
+ export function isV3Attachment(encryptedData) {
28
+ return encryptedData.startsWith(HEADER_PREFIX);
29
+ }
30
+ async function importFileKey(raw) {
31
+ return crypto.subtle.importKey('raw', raw, { name: ENCRYPTION_CONFIG.ALGORITHM }, false, [
32
+ 'encrypt',
33
+ 'decrypt',
34
+ ]);
35
+ }
36
+ /** A new file key and the header that carries it, wrapped by the master key. */
37
+ export async function createFileKey(masterKey) {
38
+ const raw = crypto.getRandomValues(new Uint8Array(FILE_KEY_BYTES));
39
+ const iv = crypto.getRandomValues(new Uint8Array(ENCRYPTION_CONFIG.IV_LENGTH));
40
+ try {
41
+ const wrapped = await crypto.subtle.encrypt({ name: ENCRYPTION_CONFIG.ALGORITHM, iv }, masterKey, raw);
42
+ return {
43
+ header: `${HEADER_PREFIX}${bytesToBase64(iv)}:${bytesToBase64(wrapped)}`,
44
+ fileKey: await importFileKey(raw),
45
+ };
46
+ }
47
+ finally {
48
+ raw.fill(0);
49
+ }
50
+ }
51
+ /** The file key out of a header. Fails (OperationError) under the wrong master key. */
52
+ export async function openFileKey(masterKey, header) {
53
+ if (!isV3Attachment(header))
54
+ throw new Error('Not a v3 attachment header');
55
+ const rest = header.slice(HEADER_PREFIX.length);
56
+ const sep = rest.indexOf(':');
57
+ if (sep === -1)
58
+ throw new Error('Malformed attachment header: missing the IV separator');
59
+ const raw = new Uint8Array(await crypto.subtle.decrypt({ name: ENCRYPTION_CONFIG.ALGORITHM, iv: base64ToBytes(rest.slice(0, sep)) }, masterKey, base64ToBytes(rest.slice(sep + 1))));
60
+ try {
61
+ if (raw.byteLength !== FILE_KEY_BYTES)
62
+ throw new Error('Malformed attachment header: wrong key length');
63
+ return await importFileKey(raw);
64
+ }
65
+ finally {
66
+ raw.fill(0);
67
+ }
68
+ }
69
+ function chunkParams(index, isLast) {
70
+ if (!Number.isInteger(index) || index < 0 || index > 0xffffffff) {
71
+ throw new Error(`Chunk index out of range: ${index}`);
72
+ }
73
+ const iv = new Uint8Array(CHUNK_NONCE_BYTES);
74
+ new DataView(iv.buffer).setUint32(iv.byteLength - 4, index);
75
+ const aad = new Uint8Array(AAD_LABEL.byteLength + 5);
76
+ aad.set(AAD_LABEL);
77
+ new DataView(aad.buffer).setUint32(AAD_LABEL.byteLength, index);
78
+ aad[aad.byteLength - 1] = isLast ? 1 : 0;
79
+ return { name: ENCRYPTION_CONFIG.ALGORITHM, iv, additionalData: aad };
80
+ }
81
+ /** Seals chunk `index` (0-based). The result is the chunk plus FILE_CHUNK_OVERHEAD bytes. */
82
+ export async function encryptFileChunk(fileKey, index, isLast, plain) {
83
+ return new Uint8Array(await crypto.subtle.encrypt(chunkParams(index, isLast), fileKey, plain));
84
+ }
85
+ /** Opens chunk `index`. Fails if it is not that chunk of that file, or was cut short. */
86
+ export async function decryptFileChunk(fileKey, index, isLast, sealed) {
87
+ return new Uint8Array(await crypto.subtle.decrypt(chunkParams(index, isLast), fileKey, sealed));
88
+ }
89
+ /** How many chunks a file of `size` bytes is cut into. */
90
+ export function fileChunkCount(size, chunkSize) {
91
+ return Math.max(1, Math.ceil(size / chunkSize));
92
+ }
93
+ /** Where sealed chunk `index` sits in the stored object: [start, end), in bytes. */
94
+ export function sealedChunkRange(size, chunkSize, index) {
95
+ const count = fileChunkCount(size, chunkSize);
96
+ const plain = index < count - 1 ? chunkSize : size - chunkSize * (count - 1);
97
+ const start = index * (chunkSize + FILE_CHUNK_OVERHEAD);
98
+ return { start, end: start + plain + FILE_CHUNK_OVERHEAD };
99
+ }
100
+ //# sourceMappingURL=chunked.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunked.js","sourceRoot":"","sources":["../src/chunked.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The format for a file too large to hold as one ciphertext: v3.
3
+ *
4
+ * The file gets a random key of its own. That key, wrapped by the account's
5
+ * master key, is the header "v3:<ivB64>:<wrappedKeyB64>", which is all the
6
+ * row's encryptedData column holds. The content is cut into chunks of a fixed
7
+ * plaintext size and each is sealed on its own with AES-256-GCM, so a reader
8
+ * can fetch and check any chunk without the rest, and neither end ever holds
9
+ * the whole file in memory.
10
+ *
11
+ * A chunk is bound to its place: its nonce is its index, and its index and
12
+ * whether it is the last chunk are authenticated with it. A store that
13
+ * reorders chunks, drops some from the end or hands back one file's chunk as
14
+ * another's is caught by the tag. The index can be the whole nonce because
15
+ * the key is used for this one file and nothing else.
16
+ */
17
+ /** AES-GCM appends its authentication tag to each chunk. */
18
+ export declare const FILE_CHUNK_OVERHEAD = 16;
19
+ export declare function isV3Attachment(encryptedData: string): boolean;
20
+ /** A new file key and the header that carries it, wrapped by the master key. */
21
+ export declare function createFileKey(masterKey: CryptoKey): Promise<{
22
+ header: string;
23
+ fileKey: CryptoKey;
24
+ }>;
25
+ /** The file key out of a header. Fails (OperationError) under the wrong master key. */
26
+ export declare function openFileKey(masterKey: CryptoKey, header: string): Promise<CryptoKey>;
27
+ /** Seals chunk `index` (0-based). The result is the chunk plus FILE_CHUNK_OVERHEAD bytes. */
28
+ export declare function encryptFileChunk(fileKey: CryptoKey, index: number, isLast: boolean, plain: Uint8Array | ArrayBuffer): Promise<Uint8Array>;
29
+ /** Opens chunk `index`. Fails if it is not that chunk of that file, or was cut short. */
30
+ export declare function decryptFileChunk(fileKey: CryptoKey, index: number, isLast: boolean, sealed: Uint8Array | ArrayBuffer): Promise<Uint8Array>;
31
+ /** How many chunks a file of `size` bytes is cut into. */
32
+ export declare function fileChunkCount(size: number, chunkSize: number): number;
33
+ /** Where sealed chunk `index` sits in the stored object: [start, end), in bytes. */
34
+ export declare function sealedChunkRange(size: number, chunkSize: number, index: number): {
35
+ start: number;
36
+ end: number;
37
+ };
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FILE_CHUNK_OVERHEAD = void 0;
4
+ exports.isV3Attachment = isV3Attachment;
5
+ exports.createFileKey = createFileKey;
6
+ exports.openFileKey = openFileKey;
7
+ exports.encryptFileChunk = encryptFileChunk;
8
+ exports.decryptFileChunk = decryptFileChunk;
9
+ exports.fileChunkCount = fileChunkCount;
10
+ exports.sealedChunkRange = sealedChunkRange;
11
+ const base64_js_1 = require("./base64.js");
12
+ const constants_js_1 = require("./constants.js");
13
+ /**
14
+ * The format for a file too large to hold as one ciphertext: v3.
15
+ *
16
+ * The file gets a random key of its own. That key, wrapped by the account's
17
+ * master key, is the header "v3:<ivB64>:<wrappedKeyB64>", which is all the
18
+ * row's encryptedData column holds. The content is cut into chunks of a fixed
19
+ * plaintext size and each is sealed on its own with AES-256-GCM, so a reader
20
+ * can fetch and check any chunk without the rest, and neither end ever holds
21
+ * the whole file in memory.
22
+ *
23
+ * A chunk is bound to its place: its nonce is its index, and its index and
24
+ * whether it is the last chunk are authenticated with it. A store that
25
+ * reorders chunks, drops some from the end or hands back one file's chunk as
26
+ * another's is caught by the tag. The index can be the whole nonce because
27
+ * the key is used for this one file and nothing else.
28
+ */
29
+ /** AES-GCM appends its authentication tag to each chunk. */
30
+ exports.FILE_CHUNK_OVERHEAD = 16;
31
+ /** The size GCM is specified for. Nothing random goes in it, so there is no
32
+ * reason for the longer IV the rest of the formats use. */
33
+ const CHUNK_NONCE_BYTES = 12;
34
+ const HEADER_PREFIX = 'v3:';
35
+ const FILE_KEY_BYTES = 32;
36
+ const AAD_LABEL = new TextEncoder().encode('virlow-file-v3');
37
+ function isV3Attachment(encryptedData) {
38
+ return encryptedData.startsWith(HEADER_PREFIX);
39
+ }
40
+ async function importFileKey(raw) {
41
+ return crypto.subtle.importKey('raw', raw, { name: constants_js_1.ENCRYPTION_CONFIG.ALGORITHM }, false, [
42
+ 'encrypt',
43
+ 'decrypt',
44
+ ]);
45
+ }
46
+ /** A new file key and the header that carries it, wrapped by the master key. */
47
+ async function createFileKey(masterKey) {
48
+ const raw = crypto.getRandomValues(new Uint8Array(FILE_KEY_BYTES));
49
+ const iv = crypto.getRandomValues(new Uint8Array(constants_js_1.ENCRYPTION_CONFIG.IV_LENGTH));
50
+ try {
51
+ const wrapped = await crypto.subtle.encrypt({ name: constants_js_1.ENCRYPTION_CONFIG.ALGORITHM, iv }, masterKey, raw);
52
+ return {
53
+ header: `${HEADER_PREFIX}${(0, base64_js_1.bytesToBase64)(iv)}:${(0, base64_js_1.bytesToBase64)(wrapped)}`,
54
+ fileKey: await importFileKey(raw),
55
+ };
56
+ }
57
+ finally {
58
+ raw.fill(0);
59
+ }
60
+ }
61
+ /** The file key out of a header. Fails (OperationError) under the wrong master key. */
62
+ async function openFileKey(masterKey, header) {
63
+ if (!isV3Attachment(header))
64
+ throw new Error('Not a v3 attachment header');
65
+ const rest = header.slice(HEADER_PREFIX.length);
66
+ const sep = rest.indexOf(':');
67
+ if (sep === -1)
68
+ throw new Error('Malformed attachment header: missing the IV separator');
69
+ const raw = new Uint8Array(await crypto.subtle.decrypt({ name: constants_js_1.ENCRYPTION_CONFIG.ALGORITHM, iv: (0, base64_js_1.base64ToBytes)(rest.slice(0, sep)) }, masterKey, (0, base64_js_1.base64ToBytes)(rest.slice(sep + 1))));
70
+ try {
71
+ if (raw.byteLength !== FILE_KEY_BYTES)
72
+ throw new Error('Malformed attachment header: wrong key length');
73
+ return await importFileKey(raw);
74
+ }
75
+ finally {
76
+ raw.fill(0);
77
+ }
78
+ }
79
+ function chunkParams(index, isLast) {
80
+ if (!Number.isInteger(index) || index < 0 || index > 0xffffffff) {
81
+ throw new Error(`Chunk index out of range: ${index}`);
82
+ }
83
+ const iv = new Uint8Array(CHUNK_NONCE_BYTES);
84
+ new DataView(iv.buffer).setUint32(iv.byteLength - 4, index);
85
+ const aad = new Uint8Array(AAD_LABEL.byteLength + 5);
86
+ aad.set(AAD_LABEL);
87
+ new DataView(aad.buffer).setUint32(AAD_LABEL.byteLength, index);
88
+ aad[aad.byteLength - 1] = isLast ? 1 : 0;
89
+ return { name: constants_js_1.ENCRYPTION_CONFIG.ALGORITHM, iv, additionalData: aad };
90
+ }
91
+ /** Seals chunk `index` (0-based). The result is the chunk plus FILE_CHUNK_OVERHEAD bytes. */
92
+ async function encryptFileChunk(fileKey, index, isLast, plain) {
93
+ return new Uint8Array(await crypto.subtle.encrypt(chunkParams(index, isLast), fileKey, plain));
94
+ }
95
+ /** Opens chunk `index`. Fails if it is not that chunk of that file, or was cut short. */
96
+ async function decryptFileChunk(fileKey, index, isLast, sealed) {
97
+ return new Uint8Array(await crypto.subtle.decrypt(chunkParams(index, isLast), fileKey, sealed));
98
+ }
99
+ /** How many chunks a file of `size` bytes is cut into. */
100
+ function fileChunkCount(size, chunkSize) {
101
+ return Math.max(1, Math.ceil(size / chunkSize));
102
+ }
103
+ /** Where sealed chunk `index` sits in the stored object: [start, end), in bytes. */
104
+ function sealedChunkRange(size, chunkSize, index) {
105
+ const count = fileChunkCount(size, chunkSize);
106
+ const plain = index < count - 1 ? chunkSize : size - chunkSize * (count - 1);
107
+ const start = index * (chunkSize + exports.FILE_CHUNK_OVERHEAD);
108
+ return { start, end: start + plain + exports.FILE_CHUNK_OVERHEAD };
109
+ }
@@ -4,5 +4,6 @@ export { ENCRYPTION_CONFIG, VERIFIER_CONSTANT } from './constants.js';
4
4
  export { deriveMasterKey, importMasterKey } from './keys.js';
5
5
  export { decryptNoteFields, decryptTitleField, encryptNoteFields, type EncryptedNoteFields, } from './notes.js';
6
6
  export { LEGACY_ATTACHMENT_MESSAGE, LegacyAttachmentError, decryptAttachment, decryptAttachmentData, encryptAttachment, encryptAttachmentName, isV2Attachment, type EncryptedAttachment, } from './attachments.js';
7
+ export { FILE_CHUNK_OVERHEAD, createFileKey, decryptFileChunk, encryptFileChunk, fileChunkCount, isV3Attachment, openFileKey, sealedChunkRange, } from './chunked.js';
7
8
  export { canDecryptWithKey, createVerifierBlob, verifyVerifierBlob, type VerifierBlob, } from './verifier.js';
8
9
  export declare const PACKAGE_NAME = "@batalabs/virlow-crypto";
package/dist/cjs/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.PACKAGE_NAME = exports.verifyVerifierBlob = exports.createVerifierBlob = exports.canDecryptWithKey = exports.isV2Attachment = exports.encryptAttachmentName = exports.encryptAttachment = exports.decryptAttachmentData = exports.decryptAttachment = exports.LegacyAttachmentError = exports.LEGACY_ATTACHMENT_MESSAGE = exports.encryptNoteFields = exports.decryptTitleField = exports.decryptNoteFields = exports.importMasterKey = exports.deriveMasterKey = exports.VERIFIER_CONSTANT = exports.ENCRYPTION_CONFIG = exports.encryptStringBlob = exports.encryptBytesBlob = exports.decryptStringBlob = exports.decryptBytesBlob = exports.bytesToBase64 = exports.base64ToBytes = exports.base64ToArrayBuffer = void 0;
3
+ exports.PACKAGE_NAME = exports.verifyVerifierBlob = exports.createVerifierBlob = exports.canDecryptWithKey = exports.sealedChunkRange = exports.openFileKey = exports.isV3Attachment = exports.fileChunkCount = exports.encryptFileChunk = exports.decryptFileChunk = exports.createFileKey = exports.FILE_CHUNK_OVERHEAD = exports.isV2Attachment = exports.encryptAttachmentName = exports.encryptAttachment = exports.decryptAttachmentData = exports.decryptAttachment = exports.LegacyAttachmentError = exports.LEGACY_ATTACHMENT_MESSAGE = exports.encryptNoteFields = exports.decryptTitleField = exports.decryptNoteFields = exports.importMasterKey = exports.deriveMasterKey = exports.VERIFIER_CONSTANT = exports.ENCRYPTION_CONFIG = exports.encryptStringBlob = exports.encryptBytesBlob = exports.decryptStringBlob = exports.decryptBytesBlob = exports.bytesToBase64 = exports.base64ToBytes = exports.base64ToArrayBuffer = void 0;
4
4
  var base64_js_1 = require("./base64.js");
5
5
  Object.defineProperty(exports, "base64ToArrayBuffer", { enumerable: true, get: function () { return base64_js_1.base64ToArrayBuffer; } });
6
6
  Object.defineProperty(exports, "base64ToBytes", { enumerable: true, get: function () { return base64_js_1.base64ToBytes; } });
@@ -28,6 +28,15 @@ Object.defineProperty(exports, "decryptAttachmentData", { enumerable: true, get:
28
28
  Object.defineProperty(exports, "encryptAttachment", { enumerable: true, get: function () { return attachments_js_1.encryptAttachment; } });
29
29
  Object.defineProperty(exports, "encryptAttachmentName", { enumerable: true, get: function () { return attachments_js_1.encryptAttachmentName; } });
30
30
  Object.defineProperty(exports, "isV2Attachment", { enumerable: true, get: function () { return attachments_js_1.isV2Attachment; } });
31
+ var chunked_js_1 = require("./chunked.js");
32
+ Object.defineProperty(exports, "FILE_CHUNK_OVERHEAD", { enumerable: true, get: function () { return chunked_js_1.FILE_CHUNK_OVERHEAD; } });
33
+ Object.defineProperty(exports, "createFileKey", { enumerable: true, get: function () { return chunked_js_1.createFileKey; } });
34
+ Object.defineProperty(exports, "decryptFileChunk", { enumerable: true, get: function () { return chunked_js_1.decryptFileChunk; } });
35
+ Object.defineProperty(exports, "encryptFileChunk", { enumerable: true, get: function () { return chunked_js_1.encryptFileChunk; } });
36
+ Object.defineProperty(exports, "fileChunkCount", { enumerable: true, get: function () { return chunked_js_1.fileChunkCount; } });
37
+ Object.defineProperty(exports, "isV3Attachment", { enumerable: true, get: function () { return chunked_js_1.isV3Attachment; } });
38
+ Object.defineProperty(exports, "openFileKey", { enumerable: true, get: function () { return chunked_js_1.openFileKey; } });
39
+ Object.defineProperty(exports, "sealedChunkRange", { enumerable: true, get: function () { return chunked_js_1.sealedChunkRange; } });
31
40
  var verifier_js_1 = require("./verifier.js");
32
41
  Object.defineProperty(exports, "canDecryptWithKey", { enumerable: true, get: function () { return verifier_js_1.canDecryptWithKey; } });
33
42
  Object.defineProperty(exports, "createVerifierBlob", { enumerable: true, get: function () { return verifier_js_1.createVerifierBlob; } });
package/dist/index.d.ts CHANGED
@@ -4,6 +4,7 @@ export { ENCRYPTION_CONFIG, VERIFIER_CONSTANT } from './constants.js';
4
4
  export { deriveMasterKey, importMasterKey } from './keys.js';
5
5
  export { decryptNoteFields, decryptTitleField, encryptNoteFields, type EncryptedNoteFields, } from './notes.js';
6
6
  export { LEGACY_ATTACHMENT_MESSAGE, LegacyAttachmentError, decryptAttachment, decryptAttachmentData, encryptAttachment, encryptAttachmentName, isV2Attachment, type EncryptedAttachment, } from './attachments.js';
7
+ export { FILE_CHUNK_OVERHEAD, createFileKey, decryptFileChunk, encryptFileChunk, fileChunkCount, isV3Attachment, openFileKey, sealedChunkRange, } from './chunked.js';
7
8
  export { canDecryptWithKey, createVerifierBlob, verifyVerifierBlob, type VerifierBlob, } from './verifier.js';
8
9
  export declare const PACKAGE_NAME = "@batalabs/virlow-crypto";
9
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAChF,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACtE,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAC7D,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EACjB,iBAAiB,EACjB,KAAK,mBAAmB,GACzB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,yBAAyB,EACzB,qBAAqB,EACrB,iBAAiB,EACjB,qBAAqB,EACrB,iBAAiB,EACjB,qBAAqB,EACrB,cAAc,EACd,KAAK,mBAAmB,GACzB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,KAAK,YAAY,GAClB,MAAM,eAAe,CAAC;AACvB,eAAO,MAAM,YAAY,4BAA4B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAChF,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACtE,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAC7D,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EACjB,iBAAiB,EACjB,KAAK,mBAAmB,GACzB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,yBAAyB,EACzB,qBAAqB,EACrB,iBAAiB,EACjB,qBAAqB,EACrB,iBAAiB,EACjB,qBAAqB,EACrB,cAAc,EACd,KAAK,mBAAmB,GACzB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,mBAAmB,EACnB,aAAa,EACb,gBAAgB,EAChB,gBAAgB,EAChB,cAAc,EACd,cAAc,EACd,WAAW,EACX,gBAAgB,GACjB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,KAAK,YAAY,GAClB,MAAM,eAAe,CAAC;AACvB,eAAO,MAAM,YAAY,4BAA4B,CAAC"}
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ export { ENCRYPTION_CONFIG, VERIFIER_CONSTANT } from './constants.js';
4
4
  export { deriveMasterKey, importMasterKey } from './keys.js';
5
5
  export { decryptNoteFields, decryptTitleField, encryptNoteFields, } from './notes.js';
6
6
  export { LEGACY_ATTACHMENT_MESSAGE, LegacyAttachmentError, decryptAttachment, decryptAttachmentData, encryptAttachment, encryptAttachmentName, isV2Attachment, } from './attachments.js';
7
+ export { FILE_CHUNK_OVERHEAD, createFileKey, decryptFileChunk, encryptFileChunk, fileChunkCount, isV3Attachment, openFileKey, sealedChunkRange, } from './chunked.js';
7
8
  export { canDecryptWithKey, createVerifierBlob, verifyVerifierBlob, } from './verifier.js';
8
9
  export const PACKAGE_NAME = '@batalabs/virlow-crypto';
9
10
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAChF,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACtE,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAC7D,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EACjB,iBAAiB,GAElB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,yBAAyB,EACzB,qBAAqB,EACrB,iBAAiB,EACjB,qBAAqB,EACrB,iBAAiB,EACjB,qBAAqB,EACrB,cAAc,GAEf,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,GAEnB,MAAM,eAAe,CAAC;AACvB,MAAM,CAAC,MAAM,YAAY,GAAG,yBAAyB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAChF,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACtE,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAC7D,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EACjB,iBAAiB,GAElB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,yBAAyB,EACzB,qBAAqB,EACrB,iBAAiB,EACjB,qBAAqB,EACrB,iBAAiB,EACjB,qBAAqB,EACrB,cAAc,GAEf,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,mBAAmB,EACnB,aAAa,EACb,gBAAgB,EAChB,gBAAgB,EAChB,cAAc,EACd,cAAc,EACd,WAAW,EACX,gBAAgB,GACjB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,GAEnB,MAAM,eAAe,CAAC;AACvB,MAAM,CAAC,MAAM,YAAY,GAAG,yBAAyB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@batalabs/virlow-crypto",
3
- "version": "3.97.0",
3
+ "version": "3.98.0",
4
4
  "type": "module",
5
5
  "description": "Virlow client-side encryption primitives (PBKDF2 + AES-256-GCM). Framework-free; browser and Node 20+.",
6
6
  "license": "UNLICENSED",