@batalabs/virlow-crypto 3.97.0 → 3.99.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 +44 -0
- package/dist/chunked.d.ts.map +1 -0
- package/dist/chunked.js +124 -0
- package/dist/chunked.js.map +1 -0
- package/dist/cjs/chunked.d.ts +43 -0
- package/dist/cjs/chunked.js +134 -0
- package/dist/cjs/index.d.ts +2 -0
- package/dist/cjs/index.js +27 -1
- package/dist/cjs/sharing.d.ts +74 -0
- package/dist/cjs/sharing.js +222 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/sharing.d.ts +75 -0
- package/dist/sharing.d.ts.map +1 -0
- package/dist/sharing.js +205 -0
- package/dist/sharing.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
/**
|
|
39
|
+
* The same file key, wrapped by another key: what sharing a workspace (or
|
|
40
|
+
* rotating its key) does to a large file. The content is untouched, so a
|
|
41
|
+
* file of any size moves over in one small write.
|
|
42
|
+
*/
|
|
43
|
+
export declare function rewrapFileKey(fromKey: CryptoKey, toKey: CryptoKey, header: string): Promise<string>;
|
|
44
|
+
//# 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;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAoBzG"}
|
package/dist/chunked.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
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
|
+
/**
|
|
101
|
+
* The same file key, wrapped by another key: what sharing a workspace (or
|
|
102
|
+
* rotating its key) does to a large file. The content is untouched, so a
|
|
103
|
+
* file of any size moves over in one small write.
|
|
104
|
+
*/
|
|
105
|
+
export async function rewrapFileKey(fromKey, toKey, header) {
|
|
106
|
+
if (!isV3Attachment(header))
|
|
107
|
+
throw new Error('Not a v3 attachment header');
|
|
108
|
+
const rest = header.slice(HEADER_PREFIX.length);
|
|
109
|
+
const sep = rest.indexOf(':');
|
|
110
|
+
if (sep === -1)
|
|
111
|
+
throw new Error('Malformed attachment header: missing the IV separator');
|
|
112
|
+
const raw = new Uint8Array(await crypto.subtle.decrypt({ name: ENCRYPTION_CONFIG.ALGORITHM, iv: base64ToBytes(rest.slice(0, sep)) }, fromKey, base64ToBytes(rest.slice(sep + 1))));
|
|
113
|
+
try {
|
|
114
|
+
if (raw.byteLength !== FILE_KEY_BYTES)
|
|
115
|
+
throw new Error('Malformed attachment header: wrong key length');
|
|
116
|
+
const iv = crypto.getRandomValues(new Uint8Array(ENCRYPTION_CONFIG.IV_LENGTH));
|
|
117
|
+
const wrapped = await crypto.subtle.encrypt({ name: ENCRYPTION_CONFIG.ALGORITHM, iv }, toKey, raw);
|
|
118
|
+
return `${HEADER_PREFIX}${bytesToBase64(iv)}:${bytesToBase64(wrapped)}`;
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
raw.fill(0);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
//# 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,43 @@
|
|
|
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
|
+
/**
|
|
39
|
+
* The same file key, wrapped by another key: what sharing a workspace (or
|
|
40
|
+
* rotating its key) does to a large file. The content is untouched, so a
|
|
41
|
+
* file of any size moves over in one small write.
|
|
42
|
+
*/
|
|
43
|
+
export declare function rewrapFileKey(fromKey: CryptoKey, toKey: CryptoKey, header: string): Promise<string>;
|
|
@@ -0,0 +1,134 @@
|
|
|
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
|
+
exports.rewrapFileKey = rewrapFileKey;
|
|
12
|
+
const base64_js_1 = require("./base64.js");
|
|
13
|
+
const constants_js_1 = require("./constants.js");
|
|
14
|
+
/**
|
|
15
|
+
* The format for a file too large to hold as one ciphertext: v3.
|
|
16
|
+
*
|
|
17
|
+
* The file gets a random key of its own. That key, wrapped by the account's
|
|
18
|
+
* master key, is the header "v3:<ivB64>:<wrappedKeyB64>", which is all the
|
|
19
|
+
* row's encryptedData column holds. The content is cut into chunks of a fixed
|
|
20
|
+
* plaintext size and each is sealed on its own with AES-256-GCM, so a reader
|
|
21
|
+
* can fetch and check any chunk without the rest, and neither end ever holds
|
|
22
|
+
* the whole file in memory.
|
|
23
|
+
*
|
|
24
|
+
* A chunk is bound to its place: its nonce is its index, and its index and
|
|
25
|
+
* whether it is the last chunk are authenticated with it. A store that
|
|
26
|
+
* reorders chunks, drops some from the end or hands back one file's chunk as
|
|
27
|
+
* another's is caught by the tag. The index can be the whole nonce because
|
|
28
|
+
* the key is used for this one file and nothing else.
|
|
29
|
+
*/
|
|
30
|
+
/** AES-GCM appends its authentication tag to each chunk. */
|
|
31
|
+
exports.FILE_CHUNK_OVERHEAD = 16;
|
|
32
|
+
/** The size GCM is specified for. Nothing random goes in it, so there is no
|
|
33
|
+
* reason for the longer IV the rest of the formats use. */
|
|
34
|
+
const CHUNK_NONCE_BYTES = 12;
|
|
35
|
+
const HEADER_PREFIX = 'v3:';
|
|
36
|
+
const FILE_KEY_BYTES = 32;
|
|
37
|
+
const AAD_LABEL = new TextEncoder().encode('virlow-file-v3');
|
|
38
|
+
function isV3Attachment(encryptedData) {
|
|
39
|
+
return encryptedData.startsWith(HEADER_PREFIX);
|
|
40
|
+
}
|
|
41
|
+
async function importFileKey(raw) {
|
|
42
|
+
return crypto.subtle.importKey('raw', raw, { name: constants_js_1.ENCRYPTION_CONFIG.ALGORITHM }, false, [
|
|
43
|
+
'encrypt',
|
|
44
|
+
'decrypt',
|
|
45
|
+
]);
|
|
46
|
+
}
|
|
47
|
+
/** A new file key and the header that carries it, wrapped by the master key. */
|
|
48
|
+
async function createFileKey(masterKey) {
|
|
49
|
+
const raw = crypto.getRandomValues(new Uint8Array(FILE_KEY_BYTES));
|
|
50
|
+
const iv = crypto.getRandomValues(new Uint8Array(constants_js_1.ENCRYPTION_CONFIG.IV_LENGTH));
|
|
51
|
+
try {
|
|
52
|
+
const wrapped = await crypto.subtle.encrypt({ name: constants_js_1.ENCRYPTION_CONFIG.ALGORITHM, iv }, masterKey, raw);
|
|
53
|
+
return {
|
|
54
|
+
header: `${HEADER_PREFIX}${(0, base64_js_1.bytesToBase64)(iv)}:${(0, base64_js_1.bytesToBase64)(wrapped)}`,
|
|
55
|
+
fileKey: await importFileKey(raw),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
raw.fill(0);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** The file key out of a header. Fails (OperationError) under the wrong master key. */
|
|
63
|
+
async function openFileKey(masterKey, header) {
|
|
64
|
+
if (!isV3Attachment(header))
|
|
65
|
+
throw new Error('Not a v3 attachment header');
|
|
66
|
+
const rest = header.slice(HEADER_PREFIX.length);
|
|
67
|
+
const sep = rest.indexOf(':');
|
|
68
|
+
if (sep === -1)
|
|
69
|
+
throw new Error('Malformed attachment header: missing the IV separator');
|
|
70
|
+
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))));
|
|
71
|
+
try {
|
|
72
|
+
if (raw.byteLength !== FILE_KEY_BYTES)
|
|
73
|
+
throw new Error('Malformed attachment header: wrong key length');
|
|
74
|
+
return await importFileKey(raw);
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
raw.fill(0);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function chunkParams(index, isLast) {
|
|
81
|
+
if (!Number.isInteger(index) || index < 0 || index > 0xffffffff) {
|
|
82
|
+
throw new Error(`Chunk index out of range: ${index}`);
|
|
83
|
+
}
|
|
84
|
+
const iv = new Uint8Array(CHUNK_NONCE_BYTES);
|
|
85
|
+
new DataView(iv.buffer).setUint32(iv.byteLength - 4, index);
|
|
86
|
+
const aad = new Uint8Array(AAD_LABEL.byteLength + 5);
|
|
87
|
+
aad.set(AAD_LABEL);
|
|
88
|
+
new DataView(aad.buffer).setUint32(AAD_LABEL.byteLength, index);
|
|
89
|
+
aad[aad.byteLength - 1] = isLast ? 1 : 0;
|
|
90
|
+
return { name: constants_js_1.ENCRYPTION_CONFIG.ALGORITHM, iv, additionalData: aad };
|
|
91
|
+
}
|
|
92
|
+
/** Seals chunk `index` (0-based). The result is the chunk plus FILE_CHUNK_OVERHEAD bytes. */
|
|
93
|
+
async function encryptFileChunk(fileKey, index, isLast, plain) {
|
|
94
|
+
return new Uint8Array(await crypto.subtle.encrypt(chunkParams(index, isLast), fileKey, plain));
|
|
95
|
+
}
|
|
96
|
+
/** Opens chunk `index`. Fails if it is not that chunk of that file, or was cut short. */
|
|
97
|
+
async function decryptFileChunk(fileKey, index, isLast, sealed) {
|
|
98
|
+
return new Uint8Array(await crypto.subtle.decrypt(chunkParams(index, isLast), fileKey, sealed));
|
|
99
|
+
}
|
|
100
|
+
/** How many chunks a file of `size` bytes is cut into. */
|
|
101
|
+
function fileChunkCount(size, chunkSize) {
|
|
102
|
+
return Math.max(1, Math.ceil(size / chunkSize));
|
|
103
|
+
}
|
|
104
|
+
/** Where sealed chunk `index` sits in the stored object: [start, end), in bytes. */
|
|
105
|
+
function sealedChunkRange(size, chunkSize, index) {
|
|
106
|
+
const count = fileChunkCount(size, chunkSize);
|
|
107
|
+
const plain = index < count - 1 ? chunkSize : size - chunkSize * (count - 1);
|
|
108
|
+
const start = index * (chunkSize + exports.FILE_CHUNK_OVERHEAD);
|
|
109
|
+
return { start, end: start + plain + exports.FILE_CHUNK_OVERHEAD };
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* The same file key, wrapped by another key: what sharing a workspace (or
|
|
113
|
+
* rotating its key) does to a large file. The content is untouched, so a
|
|
114
|
+
* file of any size moves over in one small write.
|
|
115
|
+
*/
|
|
116
|
+
async function rewrapFileKey(fromKey, toKey, header) {
|
|
117
|
+
if (!isV3Attachment(header))
|
|
118
|
+
throw new Error('Not a v3 attachment header');
|
|
119
|
+
const rest = header.slice(HEADER_PREFIX.length);
|
|
120
|
+
const sep = rest.indexOf(':');
|
|
121
|
+
if (sep === -1)
|
|
122
|
+
throw new Error('Malformed attachment header: missing the IV separator');
|
|
123
|
+
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)) }, fromKey, (0, base64_js_1.base64ToBytes)(rest.slice(sep + 1))));
|
|
124
|
+
try {
|
|
125
|
+
if (raw.byteLength !== FILE_KEY_BYTES)
|
|
126
|
+
throw new Error('Malformed attachment header: wrong key length');
|
|
127
|
+
const iv = crypto.getRandomValues(new Uint8Array(constants_js_1.ENCRYPTION_CONFIG.IV_LENGTH));
|
|
128
|
+
const wrapped = await crypto.subtle.encrypt({ name: constants_js_1.ENCRYPTION_CONFIG.ALGORITHM, iv }, toKey, raw);
|
|
129
|
+
return `${HEADER_PREFIX}${(0, base64_js_1.bytesToBase64)(iv)}:${(0, base64_js_1.bytesToBase64)(wrapped)}`;
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
raw.fill(0);
|
|
133
|
+
}
|
|
134
|
+
}
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -4,5 +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, rewrapFileKey, sealedChunkRange, } from './chunked.js';
|
|
7
8
|
export { canDecryptWithKey, createVerifierBlob, verifyVerifierBlob, type VerifierBlob, } from './verifier.js';
|
|
9
|
+
export { GrantRejectedError, createGrant, createIdentity, createWorkspaceKey, decryptUpdate, encryptUpdate, grantAad, grantStatement, openGrant, openIdentity, openSealed, safetyCode, sealTo, updateAad, workspaceKeyCommitment, type Grant, type GrantContext, type Identity, type IdentityPublic, type NewIdentity, type UpdateContext, } from './sharing.js';
|
|
8
10
|
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.workspaceKeyCommitment = exports.updateAad = exports.sealTo = exports.safetyCode = exports.openSealed = exports.openIdentity = exports.openGrant = exports.grantStatement = exports.grantAad = exports.encryptUpdate = exports.decryptUpdate = exports.createWorkspaceKey = exports.createIdentity = exports.createGrant = exports.GrantRejectedError = exports.verifyVerifierBlob = exports.createVerifierBlob = exports.canDecryptWithKey = exports.sealedChunkRange = exports.rewrapFileKey = 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,8 +28,34 @@ 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, "rewrapFileKey", { enumerable: true, get: function () { return chunked_js_1.rewrapFileKey; } });
|
|
40
|
+
Object.defineProperty(exports, "sealedChunkRange", { enumerable: true, get: function () { return chunked_js_1.sealedChunkRange; } });
|
|
31
41
|
var verifier_js_1 = require("./verifier.js");
|
|
32
42
|
Object.defineProperty(exports, "canDecryptWithKey", { enumerable: true, get: function () { return verifier_js_1.canDecryptWithKey; } });
|
|
33
43
|
Object.defineProperty(exports, "createVerifierBlob", { enumerable: true, get: function () { return verifier_js_1.createVerifierBlob; } });
|
|
34
44
|
Object.defineProperty(exports, "verifyVerifierBlob", { enumerable: true, get: function () { return verifier_js_1.verifyVerifierBlob; } });
|
|
45
|
+
var sharing_js_1 = require("./sharing.js");
|
|
46
|
+
Object.defineProperty(exports, "GrantRejectedError", { enumerable: true, get: function () { return sharing_js_1.GrantRejectedError; } });
|
|
47
|
+
Object.defineProperty(exports, "createGrant", { enumerable: true, get: function () { return sharing_js_1.createGrant; } });
|
|
48
|
+
Object.defineProperty(exports, "createIdentity", { enumerable: true, get: function () { return sharing_js_1.createIdentity; } });
|
|
49
|
+
Object.defineProperty(exports, "createWorkspaceKey", { enumerable: true, get: function () { return sharing_js_1.createWorkspaceKey; } });
|
|
50
|
+
Object.defineProperty(exports, "decryptUpdate", { enumerable: true, get: function () { return sharing_js_1.decryptUpdate; } });
|
|
51
|
+
Object.defineProperty(exports, "encryptUpdate", { enumerable: true, get: function () { return sharing_js_1.encryptUpdate; } });
|
|
52
|
+
Object.defineProperty(exports, "grantAad", { enumerable: true, get: function () { return sharing_js_1.grantAad; } });
|
|
53
|
+
Object.defineProperty(exports, "grantStatement", { enumerable: true, get: function () { return sharing_js_1.grantStatement; } });
|
|
54
|
+
Object.defineProperty(exports, "openGrant", { enumerable: true, get: function () { return sharing_js_1.openGrant; } });
|
|
55
|
+
Object.defineProperty(exports, "openIdentity", { enumerable: true, get: function () { return sharing_js_1.openIdentity; } });
|
|
56
|
+
Object.defineProperty(exports, "openSealed", { enumerable: true, get: function () { return sharing_js_1.openSealed; } });
|
|
57
|
+
Object.defineProperty(exports, "safetyCode", { enumerable: true, get: function () { return sharing_js_1.safetyCode; } });
|
|
58
|
+
Object.defineProperty(exports, "sealTo", { enumerable: true, get: function () { return sharing_js_1.sealTo; } });
|
|
59
|
+
Object.defineProperty(exports, "updateAad", { enumerable: true, get: function () { return sharing_js_1.updateAad; } });
|
|
60
|
+
Object.defineProperty(exports, "workspaceKeyCommitment", { enumerable: true, get: function () { return sharing_js_1.workspaceKeyCommitment; } });
|
|
35
61
|
exports.PACKAGE_NAME = '@batalabs/virlow-crypto';
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export interface IdentityPublic {
|
|
2
|
+
/** X25519 public key, raw, base64. */
|
|
3
|
+
box: string;
|
|
4
|
+
/** Ed25519 public key, raw, base64. */
|
|
5
|
+
sign: string;
|
|
6
|
+
}
|
|
7
|
+
export interface Identity {
|
|
8
|
+
public: IdentityPublic;
|
|
9
|
+
boxPrivate: CryptoKey;
|
|
10
|
+
signPrivate: CryptoKey;
|
|
11
|
+
}
|
|
12
|
+
export interface NewIdentity {
|
|
13
|
+
identity: Identity;
|
|
14
|
+
/** "v2:" blob, under the account key, of the private halves. */
|
|
15
|
+
sealedPrivate: string;
|
|
16
|
+
}
|
|
17
|
+
export interface GrantContext {
|
|
18
|
+
workspaceId: string;
|
|
19
|
+
keyVersion: number;
|
|
20
|
+
recipientUserId: string;
|
|
21
|
+
}
|
|
22
|
+
export interface Grant {
|
|
23
|
+
/** "s1:" blob: the workspace key, sealed to the recipient. */
|
|
24
|
+
sealed: string;
|
|
25
|
+
/** Ed25519 over the grant statement, base64. */
|
|
26
|
+
signature: string;
|
|
27
|
+
}
|
|
28
|
+
/** A new identity, and its private halves sealed under the account key. */
|
|
29
|
+
export declare function createIdentity(accountKey: CryptoKey): Promise<NewIdentity>;
|
|
30
|
+
/** The identity out of its sealed private halves. Fails under the wrong account key. */
|
|
31
|
+
export declare function openIdentity(accountKey: CryptoKey, sealedPrivate: string, publicHalves: IdentityPublic): Promise<Identity>;
|
|
32
|
+
/**
|
|
33
|
+
* The code two people compare to know they hold each other's real keys:
|
|
34
|
+
* twelve groups of five digits from SHA-512 over both public keys, each group
|
|
35
|
+
* five bytes of the digest modulo 100000, as Signal's safety numbers are.
|
|
36
|
+
*/
|
|
37
|
+
export declare function safetyCode(publicHalves: IdentityPublic): Promise<string>;
|
|
38
|
+
/** A new workspace key: the raw bytes (to seal to members, then forget) and the key. */
|
|
39
|
+
export declare function createWorkspaceKey(): Promise<{
|
|
40
|
+
raw: Uint8Array;
|
|
41
|
+
key: CryptoKey;
|
|
42
|
+
}>;
|
|
43
|
+
export declare function grantAad(context: GrantContext): Uint8Array;
|
|
44
|
+
export declare function workspaceKeyCommitment(raw: Uint8Array): Promise<Uint8Array>;
|
|
45
|
+
export declare function grantStatement(context: GrantContext, recipientBox: string, commitment: Uint8Array): Uint8Array;
|
|
46
|
+
/** Seal raw key bytes to a box public key, bound to `aad`. */
|
|
47
|
+
export declare function sealTo(recipientBox: string, raw: Uint8Array, aad: Uint8Array): Promise<string>;
|
|
48
|
+
/** The raw bytes out of a sealed blob. Fails for anyone but the recipient, or under other `aad`. */
|
|
49
|
+
export declare function openSealed(identity: Identity, sealed: string, aad: Uint8Array): Promise<Uint8Array>;
|
|
50
|
+
/** A grant of `raw` to one member, sealed to them and signed by the granter. */
|
|
51
|
+
export declare function createGrant(granter: Identity, recipientBox: string, raw: Uint8Array, context: GrantContext): Promise<Grant>;
|
|
52
|
+
export declare class GrantRejectedError extends Error {
|
|
53
|
+
constructor(message: string);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Open a grant made to this identity and check it: the seal (bound to the
|
|
57
|
+
* context) and the granter's signature over the key's commitment.
|
|
58
|
+
* `granterSign` must be the signing key this person first saw for the
|
|
59
|
+
* granter, never one just fetched without comparing.
|
|
60
|
+
*/
|
|
61
|
+
export declare function openGrant(identity: Identity, grant: Grant, context: GrantContext, granterSign: string): Promise<{
|
|
62
|
+
raw: Uint8Array;
|
|
63
|
+
key: CryptoKey;
|
|
64
|
+
}>;
|
|
65
|
+
export interface UpdateContext {
|
|
66
|
+
noteId: string;
|
|
67
|
+
keyVersion: number;
|
|
68
|
+
authorUserId: string;
|
|
69
|
+
counter: number;
|
|
70
|
+
}
|
|
71
|
+
export declare function updateAad(context: UpdateContext): Uint8Array;
|
|
72
|
+
/** A co-editing update (or snapshot, or awareness message) under the workspace key: iv || ciphertext. */
|
|
73
|
+
export declare function encryptUpdate(key: CryptoKey, update: Uint8Array, context: UpdateContext): Promise<Uint8Array>;
|
|
74
|
+
export declare function decryptUpdate(key: CryptoKey, sealed: Uint8Array, context: UpdateContext): Promise<Uint8Array>;
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GrantRejectedError = void 0;
|
|
4
|
+
exports.createIdentity = createIdentity;
|
|
5
|
+
exports.openIdentity = openIdentity;
|
|
6
|
+
exports.safetyCode = safetyCode;
|
|
7
|
+
exports.createWorkspaceKey = createWorkspaceKey;
|
|
8
|
+
exports.grantAad = grantAad;
|
|
9
|
+
exports.workspaceKeyCommitment = workspaceKeyCommitment;
|
|
10
|
+
exports.grantStatement = grantStatement;
|
|
11
|
+
exports.sealTo = sealTo;
|
|
12
|
+
exports.openSealed = openSealed;
|
|
13
|
+
exports.createGrant = createGrant;
|
|
14
|
+
exports.openGrant = openGrant;
|
|
15
|
+
exports.updateAad = updateAad;
|
|
16
|
+
exports.encryptUpdate = encryptUpdate;
|
|
17
|
+
exports.decryptUpdate = decryptUpdate;
|
|
18
|
+
const base64_js_1 = require("./base64.js");
|
|
19
|
+
const blob_js_1 = require("./blob.js");
|
|
20
|
+
const constants_js_1 = require("./constants.js");
|
|
21
|
+
/**
|
|
22
|
+
* Keys for shared workspaces (docs/superpowers/specs/2026-10-14-shared-workspaces-design.md).
|
|
23
|
+
*
|
|
24
|
+
* A person has an identity: an X25519 pair ("box") that workspace keys are
|
|
25
|
+
* sealed to, and an Ed25519 pair ("sign") that signs the grants they make.
|
|
26
|
+
* The public halves are stored in the clear; the private halves are sealed
|
|
27
|
+
* under the account key, so the server never holds them.
|
|
28
|
+
*
|
|
29
|
+
* A shared workspace has its own AES-256-GCM key, handed to each member as a
|
|
30
|
+
* grant: the key sealed to the member's box key and bound to one workspace,
|
|
31
|
+
* one key version and one person, plus the granter's signature over a
|
|
32
|
+
* commitment to the key. A member uses a key only once the grant opens and
|
|
33
|
+
* the signature checks out against the granter they remember, so a server
|
|
34
|
+
* that swaps keys or moves grants is caught.
|
|
35
|
+
*
|
|
36
|
+
* VirlowCrypto (iOS) implements the same formats; test/sharing.vectors.json
|
|
37
|
+
* holds values both sides check.
|
|
38
|
+
*/
|
|
39
|
+
const SEAL_PREFIX = 's1:';
|
|
40
|
+
const SEAL_INFO = new TextEncoder().encode('virlow-seal-v1');
|
|
41
|
+
const COMMIT_LABEL = new TextEncoder().encode('virlow-wk-commit-v1');
|
|
42
|
+
const GCM_NONCE_BYTES = 12;
|
|
43
|
+
const WORKSPACE_KEY_BYTES = 32;
|
|
44
|
+
const X25519_PUBLIC_BYTES = 32;
|
|
45
|
+
const subtle = () => crypto.subtle;
|
|
46
|
+
const encode = (text) => new TextEncoder().encode(text);
|
|
47
|
+
function concat(...parts) {
|
|
48
|
+
const out = new Uint8Array(parts.reduce((n, p) => n + p.byteLength, 0));
|
|
49
|
+
let offset = 0;
|
|
50
|
+
for (const part of parts) {
|
|
51
|
+
out.set(part, offset);
|
|
52
|
+
offset += part.byteLength;
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
async function importBoxPublic(raw) {
|
|
57
|
+
if (raw.byteLength !== X25519_PUBLIC_BYTES)
|
|
58
|
+
throw new Error('Malformed box key: wrong length');
|
|
59
|
+
return subtle().importKey('raw', raw, { name: 'X25519' }, true, []);
|
|
60
|
+
}
|
|
61
|
+
async function importSignPublic(raw) {
|
|
62
|
+
return subtle().importKey('raw', raw, { name: 'Ed25519' }, false, ['verify']);
|
|
63
|
+
}
|
|
64
|
+
async function importWorkspaceKey(raw) {
|
|
65
|
+
return subtle().importKey('raw', raw, { name: constants_js_1.ENCRYPTION_CONFIG.ALGORITHM }, false, [
|
|
66
|
+
'encrypt',
|
|
67
|
+
'decrypt',
|
|
68
|
+
]);
|
|
69
|
+
}
|
|
70
|
+
// ---------------------------------------------------------------- identity
|
|
71
|
+
/** A new identity, and its private halves sealed under the account key. */
|
|
72
|
+
async function createIdentity(accountKey) {
|
|
73
|
+
const box = (await subtle().generateKey({ name: 'X25519' }, true, ['deriveBits']));
|
|
74
|
+
const sign = (await subtle().generateKey({ name: 'Ed25519' }, true, ['sign', 'verify']));
|
|
75
|
+
const publicHalves = {
|
|
76
|
+
box: (0, base64_js_1.bytesToBase64)(await subtle().exportKey('raw', box.publicKey)),
|
|
77
|
+
sign: (0, base64_js_1.bytesToBase64)(await subtle().exportKey('raw', sign.publicKey)),
|
|
78
|
+
};
|
|
79
|
+
const privateHalves = JSON.stringify({
|
|
80
|
+
v: 1,
|
|
81
|
+
box: (0, base64_js_1.bytesToBase64)(await subtle().exportKey('pkcs8', box.privateKey)),
|
|
82
|
+
sign: (0, base64_js_1.bytesToBase64)(await subtle().exportKey('pkcs8', sign.privateKey)),
|
|
83
|
+
});
|
|
84
|
+
const sealedPrivate = await (0, blob_js_1.encryptStringBlob)(accountKey, privateHalves);
|
|
85
|
+
// Hand back keys that cannot be exported again.
|
|
86
|
+
return { identity: await openIdentity(accountKey, sealedPrivate, publicHalves), sealedPrivate };
|
|
87
|
+
}
|
|
88
|
+
/** The identity out of its sealed private halves. Fails under the wrong account key. */
|
|
89
|
+
async function openIdentity(accountKey, sealedPrivate, publicHalves) {
|
|
90
|
+
const parsed = JSON.parse(await (0, blob_js_1.decryptStringBlob)(accountKey, sealedPrivate));
|
|
91
|
+
if (parsed.v !== 1 || !parsed.box || !parsed.sign)
|
|
92
|
+
throw new Error('Unsupported identity format');
|
|
93
|
+
const boxPrivate = await subtle().importKey('pkcs8', (0, base64_js_1.base64ToBytes)(parsed.box), { name: 'X25519' }, false, [
|
|
94
|
+
'deriveBits',
|
|
95
|
+
]);
|
|
96
|
+
const signPrivate = await subtle().importKey('pkcs8', (0, base64_js_1.base64ToBytes)(parsed.sign), { name: 'Ed25519' }, false, [
|
|
97
|
+
'sign',
|
|
98
|
+
]);
|
|
99
|
+
return { public: publicHalves, boxPrivate, signPrivate };
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The code two people compare to know they hold each other's real keys:
|
|
103
|
+
* twelve groups of five digits from SHA-512 over both public keys, each group
|
|
104
|
+
* five bytes of the digest modulo 100000, as Signal's safety numbers are.
|
|
105
|
+
*/
|
|
106
|
+
async function safetyCode(publicHalves) {
|
|
107
|
+
const digest = new Uint8Array(await subtle().digest('SHA-512', concat(encode('virlow-safety-v1'), (0, base64_js_1.base64ToBytes)(publicHalves.box), (0, base64_js_1.base64ToBytes)(publicHalves.sign))));
|
|
108
|
+
const groups = [];
|
|
109
|
+
for (let i = 0; i < 12; i++) {
|
|
110
|
+
let n = 0;
|
|
111
|
+
for (let j = 0; j < 5; j++)
|
|
112
|
+
n = n * 256 + (digest[i * 5 + j] ?? 0);
|
|
113
|
+
groups.push((n % 100000).toString().padStart(5, '0'));
|
|
114
|
+
}
|
|
115
|
+
return groups.join(' ');
|
|
116
|
+
}
|
|
117
|
+
// ---------------------------------------------------------- workspace keys
|
|
118
|
+
/** A new workspace key: the raw bytes (to seal to members, then forget) and the key. */
|
|
119
|
+
async function createWorkspaceKey() {
|
|
120
|
+
const raw = crypto.getRandomValues(new Uint8Array(WORKSPACE_KEY_BYTES));
|
|
121
|
+
return { raw, key: await importWorkspaceKey(raw) };
|
|
122
|
+
}
|
|
123
|
+
function grantAad(context) {
|
|
124
|
+
return encode(`virlow-grant-v1\n${context.workspaceId}\n${context.keyVersion}\n${context.recipientUserId}`);
|
|
125
|
+
}
|
|
126
|
+
async function workspaceKeyCommitment(raw) {
|
|
127
|
+
return new Uint8Array(await subtle().digest('SHA-256', concat(COMMIT_LABEL, raw)));
|
|
128
|
+
}
|
|
129
|
+
function grantStatement(context, recipientBox, commitment) {
|
|
130
|
+
return encode(`virlow-grant-v1\n${context.workspaceId}\n${context.keyVersion}\n${context.recipientUserId}\n${recipientBox}\n${(0, base64_js_1.bytesToBase64)(commitment)}`);
|
|
131
|
+
}
|
|
132
|
+
async function sealKey(ephemeralPrivate, ephemeralPublicRaw, recipientBoxRaw) {
|
|
133
|
+
const shared = await subtle().deriveBits({ name: 'X25519', public: await importBoxPublic(recipientBoxRaw) }, ephemeralPrivate, 256);
|
|
134
|
+
const ikm = await subtle().importKey('raw', shared, 'HKDF', false, ['deriveKey']);
|
|
135
|
+
return subtle().deriveKey({ name: 'HKDF', hash: 'SHA-256', salt: concat(ephemeralPublicRaw, recipientBoxRaw), info: SEAL_INFO }, ikm, { name: constants_js_1.ENCRYPTION_CONFIG.ALGORITHM, length: 256 }, false, ['encrypt', 'decrypt']);
|
|
136
|
+
}
|
|
137
|
+
/** Seal raw key bytes to a box public key, bound to `aad`. */
|
|
138
|
+
async function sealTo(recipientBox, raw, aad) {
|
|
139
|
+
const recipientRaw = (0, base64_js_1.base64ToBytes)(recipientBox);
|
|
140
|
+
const ephemeral = (await subtle().generateKey({ name: 'X25519' }, true, ['deriveBits']));
|
|
141
|
+
const ephemeralRaw = new Uint8Array(await subtle().exportKey('raw', ephemeral.publicKey));
|
|
142
|
+
const kek = await sealKey(ephemeral.privateKey, ephemeralRaw, recipientRaw);
|
|
143
|
+
const iv = crypto.getRandomValues(new Uint8Array(GCM_NONCE_BYTES));
|
|
144
|
+
const ciphertext = new Uint8Array(await subtle().encrypt({ name: constants_js_1.ENCRYPTION_CONFIG.ALGORITHM, iv, additionalData: aad }, kek, raw));
|
|
145
|
+
return SEAL_PREFIX + (0, base64_js_1.bytesToBase64)(concat(ephemeralRaw, iv, ciphertext));
|
|
146
|
+
}
|
|
147
|
+
/** The raw bytes out of a sealed blob. Fails for anyone but the recipient, or under other `aad`. */
|
|
148
|
+
async function openSealed(identity, sealed, aad) {
|
|
149
|
+
if (!sealed.startsWith(SEAL_PREFIX))
|
|
150
|
+
throw new Error('Malformed sealed key: missing s1 prefix');
|
|
151
|
+
const bytes = (0, base64_js_1.base64ToBytes)(sealed.slice(SEAL_PREFIX.length));
|
|
152
|
+
if (bytes.byteLength < X25519_PUBLIC_BYTES + GCM_NONCE_BYTES + 16)
|
|
153
|
+
throw new Error('Malformed sealed key: too short');
|
|
154
|
+
const ephemeralRaw = bytes.subarray(0, X25519_PUBLIC_BYTES);
|
|
155
|
+
const iv = bytes.subarray(X25519_PUBLIC_BYTES, X25519_PUBLIC_BYTES + GCM_NONCE_BYTES);
|
|
156
|
+
const ciphertext = bytes.subarray(X25519_PUBLIC_BYTES + GCM_NONCE_BYTES);
|
|
157
|
+
const shared = await subtle().deriveBits({ name: 'X25519', public: await importBoxPublic(ephemeralRaw) }, identity.boxPrivate, 256);
|
|
158
|
+
const ikm = await subtle().importKey('raw', shared, 'HKDF', false, ['deriveKey']);
|
|
159
|
+
const kek = await subtle().deriveKey({
|
|
160
|
+
name: 'HKDF',
|
|
161
|
+
hash: 'SHA-256',
|
|
162
|
+
salt: concat(ephemeralRaw, (0, base64_js_1.base64ToBytes)(identity.public.box)),
|
|
163
|
+
info: SEAL_INFO,
|
|
164
|
+
}, ikm, { name: constants_js_1.ENCRYPTION_CONFIG.ALGORITHM, length: 256 }, false, ['decrypt']);
|
|
165
|
+
return new Uint8Array(await subtle().decrypt({ name: constants_js_1.ENCRYPTION_CONFIG.ALGORITHM, iv: iv, additionalData: aad }, kek, ciphertext));
|
|
166
|
+
}
|
|
167
|
+
/** A grant of `raw` to one member, sealed to them and signed by the granter. */
|
|
168
|
+
async function createGrant(granter, recipientBox, raw, context) {
|
|
169
|
+
if (raw.byteLength !== WORKSPACE_KEY_BYTES)
|
|
170
|
+
throw new Error('Workspace keys are 32 bytes');
|
|
171
|
+
const sealed = await sealTo(recipientBox, raw, grantAad(context));
|
|
172
|
+
const statement = grantStatement(context, recipientBox, await workspaceKeyCommitment(raw));
|
|
173
|
+
const signature = await subtle().sign('Ed25519', granter.signPrivate, statement);
|
|
174
|
+
return { sealed, signature: (0, base64_js_1.bytesToBase64)(signature) };
|
|
175
|
+
}
|
|
176
|
+
class GrantRejectedError extends Error {
|
|
177
|
+
constructor(message) {
|
|
178
|
+
super(message);
|
|
179
|
+
this.name = 'GrantRejectedError';
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
exports.GrantRejectedError = GrantRejectedError;
|
|
183
|
+
/**
|
|
184
|
+
* Open a grant made to this identity and check it: the seal (bound to the
|
|
185
|
+
* context) and the granter's signature over the key's commitment.
|
|
186
|
+
* `granterSign` must be the signing key this person first saw for the
|
|
187
|
+
* granter, never one just fetched without comparing.
|
|
188
|
+
*/
|
|
189
|
+
async function openGrant(identity, grant, context, granterSign) {
|
|
190
|
+
let raw;
|
|
191
|
+
try {
|
|
192
|
+
raw = await openSealed(identity, grant.sealed, grantAad(context));
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
throw new GrantRejectedError('This key was not sealed to you for this workspace');
|
|
196
|
+
}
|
|
197
|
+
if (raw.byteLength !== WORKSPACE_KEY_BYTES)
|
|
198
|
+
throw new GrantRejectedError('Workspace keys are 32 bytes');
|
|
199
|
+
const statement = grantStatement(context, identity.public.box, await workspaceKeyCommitment(raw));
|
|
200
|
+
const valid = await subtle().verify('Ed25519', await importSignPublic((0, base64_js_1.base64ToBytes)(granterSign)), (0, base64_js_1.base64ToBytes)(grant.signature), statement);
|
|
201
|
+
if (!valid)
|
|
202
|
+
throw new GrantRejectedError('The signature on this key does not match the person who shared it');
|
|
203
|
+
return { raw, key: await importWorkspaceKey(raw) };
|
|
204
|
+
}
|
|
205
|
+
function updateAad(context) {
|
|
206
|
+
return encode(`virlow-yupdate-v1\n${context.noteId}\n${context.keyVersion}\n${context.authorUserId}\n${context.counter}`);
|
|
207
|
+
}
|
|
208
|
+
/** A co-editing update (or snapshot, or awareness message) under the workspace key: iv || ciphertext. */
|
|
209
|
+
async function encryptUpdate(key, update, context) {
|
|
210
|
+
const iv = crypto.getRandomValues(new Uint8Array(GCM_NONCE_BYTES));
|
|
211
|
+
const ciphertext = await subtle().encrypt({ name: constants_js_1.ENCRYPTION_CONFIG.ALGORITHM, iv, additionalData: updateAad(context) }, key, update);
|
|
212
|
+
return concat(iv, new Uint8Array(ciphertext));
|
|
213
|
+
}
|
|
214
|
+
async function decryptUpdate(key, sealed, context) {
|
|
215
|
+
if (sealed.byteLength < GCM_NONCE_BYTES + 16)
|
|
216
|
+
throw new Error('Malformed update: too short');
|
|
217
|
+
return new Uint8Array(await subtle().decrypt({
|
|
218
|
+
name: constants_js_1.ENCRYPTION_CONFIG.ALGORITHM,
|
|
219
|
+
iv: sealed.subarray(0, GCM_NONCE_BYTES),
|
|
220
|
+
additionalData: updateAad(context),
|
|
221
|
+
}, key, sealed.subarray(GCM_NONCE_BYTES)));
|
|
222
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ 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, rewrapFileKey, sealedChunkRange, } from './chunked.js';
|
|
7
8
|
export { canDecryptWithKey, createVerifierBlob, verifyVerifierBlob, type VerifierBlob, } from './verifier.js';
|
|
9
|
+
export { GrantRejectedError, createGrant, createIdentity, createWorkspaceKey, decryptUpdate, encryptUpdate, grantAad, grantStatement, openGrant, openIdentity, openSealed, safetyCode, sealTo, updateAad, workspaceKeyCommitment, type Grant, type GrantContext, type Identity, type IdentityPublic, type NewIdentity, type UpdateContext, } from './sharing.js';
|
|
8
10
|
export declare const PACKAGE_NAME = "@batalabs/virlow-crypto";
|
|
9
11
|
//# 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,aAAa,EACb,gBAAgB,GACjB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,KAAK,YAAY,GAClB,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,kBAAkB,EAClB,WAAW,EACX,cAAc,EACd,kBAAkB,EAClB,aAAa,EACb,aAAa,EACb,QAAQ,EACR,cAAc,EACd,SAAS,EACT,YAAY,EACZ,UAAU,EACV,UAAU,EACV,MAAM,EACN,SAAS,EACT,sBAAsB,EACtB,KAAK,KAAK,EACV,KAAK,YAAY,EACjB,KAAK,QAAQ,EACb,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,aAAa,GACnB,MAAM,cAAc,CAAC;AACtB,eAAO,MAAM,YAAY,4BAA4B,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,8 @@ 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, rewrapFileKey, sealedChunkRange, } from './chunked.js';
|
|
7
8
|
export { canDecryptWithKey, createVerifierBlob, verifyVerifierBlob, } from './verifier.js';
|
|
9
|
+
export { GrantRejectedError, createGrant, createIdentity, createWorkspaceKey, decryptUpdate, encryptUpdate, grantAad, grantStatement, openGrant, openIdentity, openSealed, safetyCode, sealTo, updateAad, workspaceKeyCommitment, } from './sharing.js';
|
|
8
10
|
export const PACKAGE_NAME = '@batalabs/virlow-crypto';
|
|
9
11
|
//# 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,aAAa,EACb,gBAAgB,GACjB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,GAEnB,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,kBAAkB,EAClB,WAAW,EACX,cAAc,EACd,kBAAkB,EAClB,aAAa,EACb,aAAa,EACb,QAAQ,EACR,cAAc,EACd,SAAS,EACT,YAAY,EACZ,UAAU,EACV,UAAU,EACV,MAAM,EACN,SAAS,EACT,sBAAsB,GAOvB,MAAM,cAAc,CAAC;AACtB,MAAM,CAAC,MAAM,YAAY,GAAG,yBAAyB,CAAC"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export interface IdentityPublic {
|
|
2
|
+
/** X25519 public key, raw, base64. */
|
|
3
|
+
box: string;
|
|
4
|
+
/** Ed25519 public key, raw, base64. */
|
|
5
|
+
sign: string;
|
|
6
|
+
}
|
|
7
|
+
export interface Identity {
|
|
8
|
+
public: IdentityPublic;
|
|
9
|
+
boxPrivate: CryptoKey;
|
|
10
|
+
signPrivate: CryptoKey;
|
|
11
|
+
}
|
|
12
|
+
export interface NewIdentity {
|
|
13
|
+
identity: Identity;
|
|
14
|
+
/** "v2:" blob, under the account key, of the private halves. */
|
|
15
|
+
sealedPrivate: string;
|
|
16
|
+
}
|
|
17
|
+
export interface GrantContext {
|
|
18
|
+
workspaceId: string;
|
|
19
|
+
keyVersion: number;
|
|
20
|
+
recipientUserId: string;
|
|
21
|
+
}
|
|
22
|
+
export interface Grant {
|
|
23
|
+
/** "s1:" blob: the workspace key, sealed to the recipient. */
|
|
24
|
+
sealed: string;
|
|
25
|
+
/** Ed25519 over the grant statement, base64. */
|
|
26
|
+
signature: string;
|
|
27
|
+
}
|
|
28
|
+
/** A new identity, and its private halves sealed under the account key. */
|
|
29
|
+
export declare function createIdentity(accountKey: CryptoKey): Promise<NewIdentity>;
|
|
30
|
+
/** The identity out of its sealed private halves. Fails under the wrong account key. */
|
|
31
|
+
export declare function openIdentity(accountKey: CryptoKey, sealedPrivate: string, publicHalves: IdentityPublic): Promise<Identity>;
|
|
32
|
+
/**
|
|
33
|
+
* The code two people compare to know they hold each other's real keys:
|
|
34
|
+
* twelve groups of five digits from SHA-512 over both public keys, each group
|
|
35
|
+
* five bytes of the digest modulo 100000, as Signal's safety numbers are.
|
|
36
|
+
*/
|
|
37
|
+
export declare function safetyCode(publicHalves: IdentityPublic): Promise<string>;
|
|
38
|
+
/** A new workspace key: the raw bytes (to seal to members, then forget) and the key. */
|
|
39
|
+
export declare function createWorkspaceKey(): Promise<{
|
|
40
|
+
raw: Uint8Array;
|
|
41
|
+
key: CryptoKey;
|
|
42
|
+
}>;
|
|
43
|
+
export declare function grantAad(context: GrantContext): Uint8Array;
|
|
44
|
+
export declare function workspaceKeyCommitment(raw: Uint8Array): Promise<Uint8Array>;
|
|
45
|
+
export declare function grantStatement(context: GrantContext, recipientBox: string, commitment: Uint8Array): Uint8Array;
|
|
46
|
+
/** Seal raw key bytes to a box public key, bound to `aad`. */
|
|
47
|
+
export declare function sealTo(recipientBox: string, raw: Uint8Array, aad: Uint8Array): Promise<string>;
|
|
48
|
+
/** The raw bytes out of a sealed blob. Fails for anyone but the recipient, or under other `aad`. */
|
|
49
|
+
export declare function openSealed(identity: Identity, sealed: string, aad: Uint8Array): Promise<Uint8Array>;
|
|
50
|
+
/** A grant of `raw` to one member, sealed to them and signed by the granter. */
|
|
51
|
+
export declare function createGrant(granter: Identity, recipientBox: string, raw: Uint8Array, context: GrantContext): Promise<Grant>;
|
|
52
|
+
export declare class GrantRejectedError extends Error {
|
|
53
|
+
constructor(message: string);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Open a grant made to this identity and check it: the seal (bound to the
|
|
57
|
+
* context) and the granter's signature over the key's commitment.
|
|
58
|
+
* `granterSign` must be the signing key this person first saw for the
|
|
59
|
+
* granter, never one just fetched without comparing.
|
|
60
|
+
*/
|
|
61
|
+
export declare function openGrant(identity: Identity, grant: Grant, context: GrantContext, granterSign: string): Promise<{
|
|
62
|
+
raw: Uint8Array;
|
|
63
|
+
key: CryptoKey;
|
|
64
|
+
}>;
|
|
65
|
+
export interface UpdateContext {
|
|
66
|
+
noteId: string;
|
|
67
|
+
keyVersion: number;
|
|
68
|
+
authorUserId: string;
|
|
69
|
+
counter: number;
|
|
70
|
+
}
|
|
71
|
+
export declare function updateAad(context: UpdateContext): Uint8Array;
|
|
72
|
+
/** A co-editing update (or snapshot, or awareness message) under the workspace key: iv || ciphertext. */
|
|
73
|
+
export declare function encryptUpdate(key: CryptoKey, update: Uint8Array, context: UpdateContext): Promise<Uint8Array>;
|
|
74
|
+
export declare function decryptUpdate(key: CryptoKey, sealed: Uint8Array, context: UpdateContext): Promise<Uint8Array>;
|
|
75
|
+
//# sourceMappingURL=sharing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sharing.d.ts","sourceRoot":"","sources":["../src/sharing.ts"],"names":[],"mappings":"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"}
|
package/dist/sharing.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { base64ToBytes, bytesToBase64 } from './base64.js';
|
|
2
|
+
import { decryptStringBlob, encryptStringBlob } from './blob.js';
|
|
3
|
+
import { ENCRYPTION_CONFIG } from './constants.js';
|
|
4
|
+
/**
|
|
5
|
+
* Keys for shared workspaces (docs/superpowers/specs/2026-10-14-shared-workspaces-design.md).
|
|
6
|
+
*
|
|
7
|
+
* A person has an identity: an X25519 pair ("box") that workspace keys are
|
|
8
|
+
* sealed to, and an Ed25519 pair ("sign") that signs the grants they make.
|
|
9
|
+
* The public halves are stored in the clear; the private halves are sealed
|
|
10
|
+
* under the account key, so the server never holds them.
|
|
11
|
+
*
|
|
12
|
+
* A shared workspace has its own AES-256-GCM key, handed to each member as a
|
|
13
|
+
* grant: the key sealed to the member's box key and bound to one workspace,
|
|
14
|
+
* one key version and one person, plus the granter's signature over a
|
|
15
|
+
* commitment to the key. A member uses a key only once the grant opens and
|
|
16
|
+
* the signature checks out against the granter they remember, so a server
|
|
17
|
+
* that swaps keys or moves grants is caught.
|
|
18
|
+
*
|
|
19
|
+
* VirlowCrypto (iOS) implements the same formats; test/sharing.vectors.json
|
|
20
|
+
* holds values both sides check.
|
|
21
|
+
*/
|
|
22
|
+
const SEAL_PREFIX = 's1:';
|
|
23
|
+
const SEAL_INFO = new TextEncoder().encode('virlow-seal-v1');
|
|
24
|
+
const COMMIT_LABEL = new TextEncoder().encode('virlow-wk-commit-v1');
|
|
25
|
+
const GCM_NONCE_BYTES = 12;
|
|
26
|
+
const WORKSPACE_KEY_BYTES = 32;
|
|
27
|
+
const X25519_PUBLIC_BYTES = 32;
|
|
28
|
+
const subtle = () => crypto.subtle;
|
|
29
|
+
const encode = (text) => new TextEncoder().encode(text);
|
|
30
|
+
function concat(...parts) {
|
|
31
|
+
const out = new Uint8Array(parts.reduce((n, p) => n + p.byteLength, 0));
|
|
32
|
+
let offset = 0;
|
|
33
|
+
for (const part of parts) {
|
|
34
|
+
out.set(part, offset);
|
|
35
|
+
offset += part.byteLength;
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
async function importBoxPublic(raw) {
|
|
40
|
+
if (raw.byteLength !== X25519_PUBLIC_BYTES)
|
|
41
|
+
throw new Error('Malformed box key: wrong length');
|
|
42
|
+
return subtle().importKey('raw', raw, { name: 'X25519' }, true, []);
|
|
43
|
+
}
|
|
44
|
+
async function importSignPublic(raw) {
|
|
45
|
+
return subtle().importKey('raw', raw, { name: 'Ed25519' }, false, ['verify']);
|
|
46
|
+
}
|
|
47
|
+
async function importWorkspaceKey(raw) {
|
|
48
|
+
return subtle().importKey('raw', raw, { name: ENCRYPTION_CONFIG.ALGORITHM }, false, [
|
|
49
|
+
'encrypt',
|
|
50
|
+
'decrypt',
|
|
51
|
+
]);
|
|
52
|
+
}
|
|
53
|
+
// ---------------------------------------------------------------- identity
|
|
54
|
+
/** A new identity, and its private halves sealed under the account key. */
|
|
55
|
+
export async function createIdentity(accountKey) {
|
|
56
|
+
const box = (await subtle().generateKey({ name: 'X25519' }, true, ['deriveBits']));
|
|
57
|
+
const sign = (await subtle().generateKey({ name: 'Ed25519' }, true, ['sign', 'verify']));
|
|
58
|
+
const publicHalves = {
|
|
59
|
+
box: bytesToBase64(await subtle().exportKey('raw', box.publicKey)),
|
|
60
|
+
sign: bytesToBase64(await subtle().exportKey('raw', sign.publicKey)),
|
|
61
|
+
};
|
|
62
|
+
const privateHalves = JSON.stringify({
|
|
63
|
+
v: 1,
|
|
64
|
+
box: bytesToBase64(await subtle().exportKey('pkcs8', box.privateKey)),
|
|
65
|
+
sign: bytesToBase64(await subtle().exportKey('pkcs8', sign.privateKey)),
|
|
66
|
+
});
|
|
67
|
+
const sealedPrivate = await encryptStringBlob(accountKey, privateHalves);
|
|
68
|
+
// Hand back keys that cannot be exported again.
|
|
69
|
+
return { identity: await openIdentity(accountKey, sealedPrivate, publicHalves), sealedPrivate };
|
|
70
|
+
}
|
|
71
|
+
/** The identity out of its sealed private halves. Fails under the wrong account key. */
|
|
72
|
+
export async function openIdentity(accountKey, sealedPrivate, publicHalves) {
|
|
73
|
+
const parsed = JSON.parse(await decryptStringBlob(accountKey, sealedPrivate));
|
|
74
|
+
if (parsed.v !== 1 || !parsed.box || !parsed.sign)
|
|
75
|
+
throw new Error('Unsupported identity format');
|
|
76
|
+
const boxPrivate = await subtle().importKey('pkcs8', base64ToBytes(parsed.box), { name: 'X25519' }, false, [
|
|
77
|
+
'deriveBits',
|
|
78
|
+
]);
|
|
79
|
+
const signPrivate = await subtle().importKey('pkcs8', base64ToBytes(parsed.sign), { name: 'Ed25519' }, false, [
|
|
80
|
+
'sign',
|
|
81
|
+
]);
|
|
82
|
+
return { public: publicHalves, boxPrivate, signPrivate };
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The code two people compare to know they hold each other's real keys:
|
|
86
|
+
* twelve groups of five digits from SHA-512 over both public keys, each group
|
|
87
|
+
* five bytes of the digest modulo 100000, as Signal's safety numbers are.
|
|
88
|
+
*/
|
|
89
|
+
export async function safetyCode(publicHalves) {
|
|
90
|
+
const digest = new Uint8Array(await subtle().digest('SHA-512', concat(encode('virlow-safety-v1'), base64ToBytes(publicHalves.box), base64ToBytes(publicHalves.sign))));
|
|
91
|
+
const groups = [];
|
|
92
|
+
for (let i = 0; i < 12; i++) {
|
|
93
|
+
let n = 0;
|
|
94
|
+
for (let j = 0; j < 5; j++)
|
|
95
|
+
n = n * 256 + (digest[i * 5 + j] ?? 0);
|
|
96
|
+
groups.push((n % 100000).toString().padStart(5, '0'));
|
|
97
|
+
}
|
|
98
|
+
return groups.join(' ');
|
|
99
|
+
}
|
|
100
|
+
// ---------------------------------------------------------- workspace keys
|
|
101
|
+
/** A new workspace key: the raw bytes (to seal to members, then forget) and the key. */
|
|
102
|
+
export async function createWorkspaceKey() {
|
|
103
|
+
const raw = crypto.getRandomValues(new Uint8Array(WORKSPACE_KEY_BYTES));
|
|
104
|
+
return { raw, key: await importWorkspaceKey(raw) };
|
|
105
|
+
}
|
|
106
|
+
export function grantAad(context) {
|
|
107
|
+
return encode(`virlow-grant-v1\n${context.workspaceId}\n${context.keyVersion}\n${context.recipientUserId}`);
|
|
108
|
+
}
|
|
109
|
+
export async function workspaceKeyCommitment(raw) {
|
|
110
|
+
return new Uint8Array(await subtle().digest('SHA-256', concat(COMMIT_LABEL, raw)));
|
|
111
|
+
}
|
|
112
|
+
export function grantStatement(context, recipientBox, commitment) {
|
|
113
|
+
return encode(`virlow-grant-v1\n${context.workspaceId}\n${context.keyVersion}\n${context.recipientUserId}\n${recipientBox}\n${bytesToBase64(commitment)}`);
|
|
114
|
+
}
|
|
115
|
+
async function sealKey(ephemeralPrivate, ephemeralPublicRaw, recipientBoxRaw) {
|
|
116
|
+
const shared = await subtle().deriveBits({ name: 'X25519', public: await importBoxPublic(recipientBoxRaw) }, ephemeralPrivate, 256);
|
|
117
|
+
const ikm = await subtle().importKey('raw', shared, 'HKDF', false, ['deriveKey']);
|
|
118
|
+
return subtle().deriveKey({ name: 'HKDF', hash: 'SHA-256', salt: concat(ephemeralPublicRaw, recipientBoxRaw), info: SEAL_INFO }, ikm, { name: ENCRYPTION_CONFIG.ALGORITHM, length: 256 }, false, ['encrypt', 'decrypt']);
|
|
119
|
+
}
|
|
120
|
+
/** Seal raw key bytes to a box public key, bound to `aad`. */
|
|
121
|
+
export async function sealTo(recipientBox, raw, aad) {
|
|
122
|
+
const recipientRaw = base64ToBytes(recipientBox);
|
|
123
|
+
const ephemeral = (await subtle().generateKey({ name: 'X25519' }, true, ['deriveBits']));
|
|
124
|
+
const ephemeralRaw = new Uint8Array(await subtle().exportKey('raw', ephemeral.publicKey));
|
|
125
|
+
const kek = await sealKey(ephemeral.privateKey, ephemeralRaw, recipientRaw);
|
|
126
|
+
const iv = crypto.getRandomValues(new Uint8Array(GCM_NONCE_BYTES));
|
|
127
|
+
const ciphertext = new Uint8Array(await subtle().encrypt({ name: ENCRYPTION_CONFIG.ALGORITHM, iv, additionalData: aad }, kek, raw));
|
|
128
|
+
return SEAL_PREFIX + bytesToBase64(concat(ephemeralRaw, iv, ciphertext));
|
|
129
|
+
}
|
|
130
|
+
/** The raw bytes out of a sealed blob. Fails for anyone but the recipient, or under other `aad`. */
|
|
131
|
+
export async function openSealed(identity, sealed, aad) {
|
|
132
|
+
if (!sealed.startsWith(SEAL_PREFIX))
|
|
133
|
+
throw new Error('Malformed sealed key: missing s1 prefix');
|
|
134
|
+
const bytes = base64ToBytes(sealed.slice(SEAL_PREFIX.length));
|
|
135
|
+
if (bytes.byteLength < X25519_PUBLIC_BYTES + GCM_NONCE_BYTES + 16)
|
|
136
|
+
throw new Error('Malformed sealed key: too short');
|
|
137
|
+
const ephemeralRaw = bytes.subarray(0, X25519_PUBLIC_BYTES);
|
|
138
|
+
const iv = bytes.subarray(X25519_PUBLIC_BYTES, X25519_PUBLIC_BYTES + GCM_NONCE_BYTES);
|
|
139
|
+
const ciphertext = bytes.subarray(X25519_PUBLIC_BYTES + GCM_NONCE_BYTES);
|
|
140
|
+
const shared = await subtle().deriveBits({ name: 'X25519', public: await importBoxPublic(ephemeralRaw) }, identity.boxPrivate, 256);
|
|
141
|
+
const ikm = await subtle().importKey('raw', shared, 'HKDF', false, ['deriveKey']);
|
|
142
|
+
const kek = await subtle().deriveKey({
|
|
143
|
+
name: 'HKDF',
|
|
144
|
+
hash: 'SHA-256',
|
|
145
|
+
salt: concat(ephemeralRaw, base64ToBytes(identity.public.box)),
|
|
146
|
+
info: SEAL_INFO,
|
|
147
|
+
}, ikm, { name: ENCRYPTION_CONFIG.ALGORITHM, length: 256 }, false, ['decrypt']);
|
|
148
|
+
return new Uint8Array(await subtle().decrypt({ name: ENCRYPTION_CONFIG.ALGORITHM, iv: iv, additionalData: aad }, kek, ciphertext));
|
|
149
|
+
}
|
|
150
|
+
/** A grant of `raw` to one member, sealed to them and signed by the granter. */
|
|
151
|
+
export async function createGrant(granter, recipientBox, raw, context) {
|
|
152
|
+
if (raw.byteLength !== WORKSPACE_KEY_BYTES)
|
|
153
|
+
throw new Error('Workspace keys are 32 bytes');
|
|
154
|
+
const sealed = await sealTo(recipientBox, raw, grantAad(context));
|
|
155
|
+
const statement = grantStatement(context, recipientBox, await workspaceKeyCommitment(raw));
|
|
156
|
+
const signature = await subtle().sign('Ed25519', granter.signPrivate, statement);
|
|
157
|
+
return { sealed, signature: bytesToBase64(signature) };
|
|
158
|
+
}
|
|
159
|
+
export class GrantRejectedError extends Error {
|
|
160
|
+
constructor(message) {
|
|
161
|
+
super(message);
|
|
162
|
+
this.name = 'GrantRejectedError';
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Open a grant made to this identity and check it: the seal (bound to the
|
|
167
|
+
* context) and the granter's signature over the key's commitment.
|
|
168
|
+
* `granterSign` must be the signing key this person first saw for the
|
|
169
|
+
* granter, never one just fetched without comparing.
|
|
170
|
+
*/
|
|
171
|
+
export async function openGrant(identity, grant, context, granterSign) {
|
|
172
|
+
let raw;
|
|
173
|
+
try {
|
|
174
|
+
raw = await openSealed(identity, grant.sealed, grantAad(context));
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
throw new GrantRejectedError('This key was not sealed to you for this workspace');
|
|
178
|
+
}
|
|
179
|
+
if (raw.byteLength !== WORKSPACE_KEY_BYTES)
|
|
180
|
+
throw new GrantRejectedError('Workspace keys are 32 bytes');
|
|
181
|
+
const statement = grantStatement(context, identity.public.box, await workspaceKeyCommitment(raw));
|
|
182
|
+
const valid = await subtle().verify('Ed25519', await importSignPublic(base64ToBytes(granterSign)), base64ToBytes(grant.signature), statement);
|
|
183
|
+
if (!valid)
|
|
184
|
+
throw new GrantRejectedError('The signature on this key does not match the person who shared it');
|
|
185
|
+
return { raw, key: await importWorkspaceKey(raw) };
|
|
186
|
+
}
|
|
187
|
+
export function updateAad(context) {
|
|
188
|
+
return encode(`virlow-yupdate-v1\n${context.noteId}\n${context.keyVersion}\n${context.authorUserId}\n${context.counter}`);
|
|
189
|
+
}
|
|
190
|
+
/** A co-editing update (or snapshot, or awareness message) under the workspace key: iv || ciphertext. */
|
|
191
|
+
export async function encryptUpdate(key, update, context) {
|
|
192
|
+
const iv = crypto.getRandomValues(new Uint8Array(GCM_NONCE_BYTES));
|
|
193
|
+
const ciphertext = await subtle().encrypt({ name: ENCRYPTION_CONFIG.ALGORITHM, iv, additionalData: updateAad(context) }, key, update);
|
|
194
|
+
return concat(iv, new Uint8Array(ciphertext));
|
|
195
|
+
}
|
|
196
|
+
export async function decryptUpdate(key, sealed, context) {
|
|
197
|
+
if (sealed.byteLength < GCM_NONCE_BYTES + 16)
|
|
198
|
+
throw new Error('Malformed update: too short');
|
|
199
|
+
return new Uint8Array(await subtle().decrypt({
|
|
200
|
+
name: ENCRYPTION_CONFIG.ALGORITHM,
|
|
201
|
+
iv: sealed.subarray(0, GCM_NONCE_BYTES),
|
|
202
|
+
additionalData: updateAad(context),
|
|
203
|
+
}, key, sealed.subarray(GCM_NONCE_BYTES)));
|
|
204
|
+
}
|
|
205
|
+
//# sourceMappingURL=sharing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sharing.js","sourceRoot":"","sources":["../src/sharing.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED