@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.
- package/dist/chunked.d.ts +38 -0
- package/dist/chunked.d.ts.map +1 -0
- package/dist/chunked.js +100 -0
- package/dist/chunked.js.map +1 -0
- package/dist/cjs/chunked.d.ts +37 -0
- package/dist/cjs/chunked.js +109 -0
- package/dist/cjs/index.d.ts +1 -0
- package/dist/cjs/index.js +10 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -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"}
|
package/dist/chunked.js
ADDED
|
@@ -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
|
+
}
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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