@filefreak.io/sdk 0.3.0 → 0.3.1
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/generated.d.ts +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/shards.d.ts +61 -2
- package/dist/shards.js +162 -6
- package/package.json +1 -1
package/dist/generated.d.ts
CHANGED
|
@@ -436,8 +436,8 @@ export type UsageGetAnswer = {
|
|
|
436
436
|
plan: string;
|
|
437
437
|
storageUsedBytes: number;
|
|
438
438
|
storageIncludedBytes: number;
|
|
439
|
-
/** Where uploads are refused: what the plan includes, plus what the account's spending limit allows beyond it. */
|
|
440
|
-
storageLimitBytes: number;
|
|
439
|
+
/** Where uploads are refused: what the plan includes, plus what the account's spending limit allows beyond it. Null when nothing is: Pro that FileFreak has given the account has no storage limit. */
|
|
440
|
+
storageLimitBytes: number | null;
|
|
441
441
|
downloadedBytesThisMonth: number;
|
|
442
442
|
downloadsIncludedBytes: number;
|
|
443
443
|
apiCallsThisMonth: number;
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,6 @@ export { FileFreak, Walk, type DownloadOptions, type FollowOptions } from "./cli
|
|
|
2
2
|
export * from "./generated.js";
|
|
3
3
|
export { get, type GetOptions, type Got, type Into, type LinkedFile } from "./links.js";
|
|
4
4
|
export { type Sink } from "./ranges.js";
|
|
5
|
-
export { openFragment, sealFragment, SHARD_OVERHEAD, SHARD_VERSION, ShardError, shardKey, ShardLayout, type FragmentAt, type ShardErrorCode, type ShardKey, } from "./shards.js";
|
|
5
|
+
export { checkOutline, fileKeys, MOST_BLOCK_BYTES, openBlock, openFragment, outlineMac, sealBlock, sealFragment, SHARD_OVERHEAD, SHARD_VERSION, SHARD_VERSION_2, ShardError, shardKey, ShardLayout, type FileKeys, type FragmentAt, type Outline, type OutlineBlock, type ShardErrorCode, type ShardKey, } from "./shards.js";
|
|
6
6
|
export { FileFreakError, type CallOptions, type ClientOptions, type TokenSource } from "./transport.js";
|
|
7
7
|
export { UploadStopped, type Source, type UploadOptions } from "./uploads.js";
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,6 @@
|
|
|
12
12
|
export { FileFreak, Walk } from "./client.js";
|
|
13
13
|
export * from "./generated.js";
|
|
14
14
|
export { get } from "./links.js";
|
|
15
|
-
export { openFragment, sealFragment, SHARD_OVERHEAD, SHARD_VERSION, ShardError, shardKey, ShardLayout, } from "./shards.js";
|
|
15
|
+
export { checkOutline, fileKeys, MOST_BLOCK_BYTES, openBlock, openFragment, outlineMac, sealBlock, sealFragment, SHARD_OVERHEAD, SHARD_VERSION, SHARD_VERSION_2, ShardError, shardKey, ShardLayout, } from "./shards.js";
|
|
16
16
|
export { FileFreakError } from "./transport.js";
|
|
17
17
|
export { UploadStopped } from "./uploads.js";
|
package/dist/shards.d.ts
CHANGED
|
@@ -4,9 +4,10 @@ export declare const SHARD_OVERHEAD = 29;
|
|
|
4
4
|
/**
|
|
5
5
|
* Why a fragment or a description was refused. A reader checks in this
|
|
6
6
|
* order, and says the first that fails: short, version, length,
|
|
7
|
-
* authentication. layout is a description of a file no
|
|
7
|
+
* authentication, and in version 2 id. layout is a description of a file no
|
|
8
|
+
* fragment can match; outline is a version-2 outline whose MAC does not hold.
|
|
8
9
|
*/
|
|
9
|
-
export type ShardErrorCode = "short" | "version" | "length" | "authentication" | "layout";
|
|
10
|
+
export type ShardErrorCode = "short" | "version" | "length" | "authentication" | "id" | "layout" | "outline";
|
|
10
11
|
export declare class ShardError extends Error {
|
|
11
12
|
readonly code: ShardErrorCode;
|
|
12
13
|
constructor(code: ShardErrorCode, message: string);
|
|
@@ -57,3 +58,61 @@ export declare function sealWithNonce(key: ShardKey, at: FragmentAt, plaintext:
|
|
|
57
58
|
* returned: WebCrypto gives no plaintext until the tag has been checked.
|
|
58
59
|
*/
|
|
59
60
|
export declare function openFragment(key: ShardKey, at: FragmentAt, blob: Uint8Array): Promise<Uint8Array<ArrayBuffer>>;
|
|
61
|
+
export declare const SHARD_VERSION_2 = 2;
|
|
62
|
+
/** The most bytes of a file one block holds, as the API takes blocks. */
|
|
63
|
+
export declare const MOST_BLOCK_BYTES: number;
|
|
64
|
+
/** A block id: the HMAC-SHA256 of its bytes under the file's block key. */
|
|
65
|
+
export declare const BLOCK_ID_BYTES = 32;
|
|
66
|
+
/**
|
|
67
|
+
* A file's key in version 2, as the three keys derived from it: never the
|
|
68
|
+
* key itself, so that sealing, naming blocks and vouching for the outline
|
|
69
|
+
* cannot be played against each other.
|
|
70
|
+
*/
|
|
71
|
+
export interface FileKeys {
|
|
72
|
+
seal: ShardKey;
|
|
73
|
+
block: ShardKey;
|
|
74
|
+
outline: ShardKey;
|
|
75
|
+
}
|
|
76
|
+
/** One block as an outline gives it: its id and how many bytes of the file it holds. */
|
|
77
|
+
export interface OutlineBlock {
|
|
78
|
+
id: Uint8Array;
|
|
79
|
+
length: number;
|
|
80
|
+
}
|
|
81
|
+
/** What a content in version 2 is, as its outline gives it, before any block is fetched. */
|
|
82
|
+
export interface Outline {
|
|
83
|
+
contentId: string;
|
|
84
|
+
size: number;
|
|
85
|
+
blocks: OutlineBlock[];
|
|
86
|
+
}
|
|
87
|
+
/** The three keys of a file's key: HKDF-SHA256 with no salt, each named by its use. */
|
|
88
|
+
export declare function fileKeys(raw: Uint8Array): Promise<FileKeys>;
|
|
89
|
+
/** A block's id: what the map holds in place of a hash anybody could make of the same bytes. */
|
|
90
|
+
export declare function blockId(keys: FileKeys, plaintext: Uint8Array): Promise<Uint8Array<ArrayBuffer>>;
|
|
91
|
+
/** The bytes a block's tag also covers: the label and its id, and nothing about where it is, so that a later version can keep it as it is. */
|
|
92
|
+
export declare function blockAssociatedData(id: Uint8Array): Uint8Array<ArrayBuffer>;
|
|
93
|
+
/** One block, encrypted and ready to store under a nonce of its own, with its id. */
|
|
94
|
+
export declare function sealBlock(keys: FileKeys, plaintext: Uint8Array): Promise<{
|
|
95
|
+
id: Uint8Array<ArrayBuffer>;
|
|
96
|
+
blob: Uint8Array<ArrayBuffer>;
|
|
97
|
+
}>;
|
|
98
|
+
/**
|
|
99
|
+
* sealBlock with the id and the nonce chosen by the caller, which only the
|
|
100
|
+
* test files may do, as with sealWithNonce. Not exported from the package.
|
|
101
|
+
*/
|
|
102
|
+
export declare function sealBlockWithNonce(keys: FileKeys, id: Uint8Array, plaintext: Uint8Array, nonce: Uint8Array): Promise<Uint8Array<ArrayBuffer>>;
|
|
103
|
+
/**
|
|
104
|
+
* A stored block's bytes of the file, once it is known to be the block the
|
|
105
|
+
* outline names, unchanged: in this order, short, version, length,
|
|
106
|
+
* authentication (the tag, which binds it to its id), and id (that its
|
|
107
|
+
* bytes are the ones the id was made of). Nothing of a block that fails is
|
|
108
|
+
* returned.
|
|
109
|
+
*/
|
|
110
|
+
export declare function openBlock(keys: FileKeys, block: OutlineBlock, blob: Uint8Array): Promise<Uint8Array<ArrayBuffer>>;
|
|
111
|
+
/** The outline's MAC, which the writer sends with the upload's completion and every reader checks before it releases a byte. */
|
|
112
|
+
export declare function outlineMac(keys: FileKeys, outline: Outline): Promise<Uint8Array<ArrayBuffer>>;
|
|
113
|
+
/**
|
|
114
|
+
* Refuse an outline unless its MAC holds under the file's key: whoever has
|
|
115
|
+
* the map and not the key cannot reorder, drop, repeat or swap a content's
|
|
116
|
+
* blocks, or give one content's blocks as another's.
|
|
117
|
+
*/
|
|
118
|
+
export declare function checkOutline(keys: FileKeys, outline: Outline, mac: Uint8Array): Promise<void>;
|
package/dist/shards.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
// The shard format
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
1
|
+
// The shard format: how a file under Shard Protection is kept, as fragments
|
|
2
|
+
// each encrypted on its own. Version 1 cuts a content into fragments of one
|
|
3
|
+
// size, each sealed to its content's own key, id, place and count. Version 2
|
|
4
|
+
// cuts it into blocks by content, sealed under a key the file's versions
|
|
5
|
+
// share, so that a block a new version still has can be kept as it is.
|
|
6
|
+
// docs/reference/shard-format.md is the definition, and
|
|
7
|
+
// docs/reference/shard-format-v1.json and -v2.json hold the test files this
|
|
8
|
+
// is checked against (test/shards.test.ts), as the CLI's copy is: a writer
|
|
9
|
+
// here whose bytes differ by one from the CLI's makes files the CLI cannot
|
|
10
|
+
// read, and the reverse.
|
|
7
11
|
//
|
|
8
12
|
// WebCrypto, so the same code runs in a page, in a worker and in Node.
|
|
9
13
|
export const SHARD_VERSION = 1;
|
|
@@ -128,6 +132,158 @@ export async function openFragment(key, at, blob) {
|
|
|
128
132
|
}
|
|
129
133
|
return new Uint8Array(plain);
|
|
130
134
|
}
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// Version 2: blocks cut by content, under a file's key that versions share
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
export const SHARD_VERSION_2 = 2;
|
|
139
|
+
/** The most bytes of a file one block holds, as the API takes blocks. */
|
|
140
|
+
export const MOST_BLOCK_BYTES = 64 * 1024 * 1024;
|
|
141
|
+
/** A block id: the HMAC-SHA256 of its bytes under the file's block key. */
|
|
142
|
+
export const BLOCK_ID_BYTES = 32;
|
|
143
|
+
const LABEL_2 = new TextEncoder().encode("filefreak-shard-v2");
|
|
144
|
+
const OUTLINE_LABEL = new TextEncoder().encode("filefreak-outline-v2");
|
|
145
|
+
/** The three keys of a file's key: HKDF-SHA256 with no salt, each named by its use. */
|
|
146
|
+
export async function fileKeys(raw) {
|
|
147
|
+
if (raw.length !== 32)
|
|
148
|
+
throw new RangeError(`A shard key is 32 bytes, not ${raw.length}.`);
|
|
149
|
+
const base = await crypto.subtle.importKey("raw", new Uint8Array(raw), "HKDF", false, ["deriveBits"]);
|
|
150
|
+
const derive = (use) => crypto.subtle.deriveBits({ name: "HKDF", hash: "SHA-256", salt: new Uint8Array(0), info: new TextEncoder().encode(`filefreak-shard-v2 ${use}`) }, base, 256);
|
|
151
|
+
const [seal, block, outline] = await Promise.all([derive("seal"), derive("block"), derive("outline")]);
|
|
152
|
+
const hmac = (raw) => crypto.subtle.importKey("raw", raw, { name: "HMAC", hash: "SHA-256" }, false, ["sign", "verify"]);
|
|
153
|
+
return {
|
|
154
|
+
seal: await crypto.subtle.importKey("raw", seal, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]),
|
|
155
|
+
block: await hmac(block),
|
|
156
|
+
outline: await hmac(outline),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
/** A block's id: what the map holds in place of a hash anybody could make of the same bytes. */
|
|
160
|
+
export async function blockId(keys, plaintext) {
|
|
161
|
+
return new Uint8Array(await crypto.subtle.sign("HMAC", keys.block, new Uint8Array(plaintext)));
|
|
162
|
+
}
|
|
163
|
+
/** The bytes a block's tag also covers: the label and its id, and nothing about where it is, so that a later version can keep it as it is. */
|
|
164
|
+
export function blockAssociatedData(id) {
|
|
165
|
+
if (id.length !== BLOCK_ID_BYTES)
|
|
166
|
+
throw new RangeError(`A block id is ${BLOCK_ID_BYTES} bytes, not ${id.length}.`);
|
|
167
|
+
const out = new Uint8Array(LABEL_2.length + BLOCK_ID_BYTES);
|
|
168
|
+
out.set(LABEL_2, 0);
|
|
169
|
+
out.set(id, LABEL_2.length);
|
|
170
|
+
return out;
|
|
171
|
+
}
|
|
172
|
+
/** One block, encrypted and ready to store under a nonce of its own, with its id. */
|
|
173
|
+
export async function sealBlock(keys, plaintext) {
|
|
174
|
+
const id = await blockId(keys, plaintext);
|
|
175
|
+
return { id, blob: await sealBlockWithNonce(keys, id, plaintext, crypto.getRandomValues(new Uint8Array(NONCE_BYTES))) };
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* sealBlock with the id and the nonce chosen by the caller, which only the
|
|
179
|
+
* test files may do, as with sealWithNonce. Not exported from the package.
|
|
180
|
+
*/
|
|
181
|
+
export async function sealBlockWithNonce(keys, id, plaintext, nonce) {
|
|
182
|
+
if (plaintext.length > MOST_BLOCK_BYTES)
|
|
183
|
+
throw new ShardError("length", `A block holds at most ${MOST_BLOCK_BYTES} bytes of the file, not ${plaintext.length}.`);
|
|
184
|
+
if (nonce.length !== NONCE_BYTES)
|
|
185
|
+
throw new RangeError(`A nonce is ${NONCE_BYTES} bytes, not ${nonce.length}.`);
|
|
186
|
+
const iv = new Uint8Array(nonce);
|
|
187
|
+
const sealed = await crypto.subtle.encrypt({ name: "AES-GCM", iv, additionalData: blockAssociatedData(id), tagLength: 128 }, keys.seal, new Uint8Array(plaintext));
|
|
188
|
+
const out = new Uint8Array(HEADER_BYTES + sealed.byteLength);
|
|
189
|
+
out[0] = SHARD_VERSION_2;
|
|
190
|
+
out.set(iv, 1);
|
|
191
|
+
out.set(new Uint8Array(sealed), HEADER_BYTES);
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* A stored block's bytes of the file, once it is known to be the block the
|
|
196
|
+
* outline names, unchanged: in this order, short, version, length,
|
|
197
|
+
* authentication (the tag, which binds it to its id), and id (that its
|
|
198
|
+
* bytes are the ones the id was made of). Nothing of a block that fails is
|
|
199
|
+
* returned.
|
|
200
|
+
*/
|
|
201
|
+
export async function openBlock(keys, block, blob) {
|
|
202
|
+
const named = hexOf(block.id).slice(0, 12);
|
|
203
|
+
if (blob.length < SHARD_OVERHEAD) {
|
|
204
|
+
throw new ShardError("short", `Block ${named} is ${blob.length} bytes, shorter than any block can be (${SHARD_OVERHEAD}).`);
|
|
205
|
+
}
|
|
206
|
+
if (blob[0] !== SHARD_VERSION_2) {
|
|
207
|
+
throw new ShardError("version", `Block ${named} is in version ${blob[0]} of the format, where version ${SHARD_VERSION_2} was expected.`);
|
|
208
|
+
}
|
|
209
|
+
if (blob.length !== block.length + SHARD_OVERHEAD) {
|
|
210
|
+
throw new ShardError("length", `Block ${named} is ${blob.length} bytes, and holding ${block.length} bytes of the file it would be ${block.length + SHARD_OVERHEAD}.`);
|
|
211
|
+
}
|
|
212
|
+
let plain;
|
|
213
|
+
try {
|
|
214
|
+
plain = await crypto.subtle.decrypt({ name: "AES-GCM", iv: new Uint8Array(blob.subarray(1, HEADER_BYTES)), additionalData: blockAssociatedData(block.id), tagLength: 128 }, keys.seal, new Uint8Array(blob.subarray(HEADER_BYTES)));
|
|
215
|
+
}
|
|
216
|
+
catch (e) {
|
|
217
|
+
if (e instanceof DOMException && e.name === "OperationError") {
|
|
218
|
+
throw new ShardError("authentication", `Block ${named} is not what was written as this file's block ${named}: it has been changed, or it belongs to another file or block.`);
|
|
219
|
+
}
|
|
220
|
+
throw e;
|
|
221
|
+
}
|
|
222
|
+
const bytes = new Uint8Array(plain);
|
|
223
|
+
// The tag says somebody with the key sealed these bytes under this id; this says the id is theirs.
|
|
224
|
+
if (!(await crypto.subtle.verify("HMAC", keys.block, new Uint8Array(block.id), bytes))) {
|
|
225
|
+
throw new ShardError("id", `Block ${named} opens, but its bytes are not the ones its id was made of.`);
|
|
226
|
+
}
|
|
227
|
+
return bytes;
|
|
228
|
+
}
|
|
229
|
+
/** Refuse an outline that describes no file: no blocks, a block too long, an empty block in a file that is not empty, or lengths that do not add up. */
|
|
230
|
+
function checkShape(outline) {
|
|
231
|
+
const { size, blocks } = outline;
|
|
232
|
+
if (!Number.isSafeInteger(size) || size < 0)
|
|
233
|
+
throw new ShardError("layout", `A file's size is a whole number of bytes, not ${size}.`);
|
|
234
|
+
if (blocks.length === 0)
|
|
235
|
+
throw new ShardError("layout", "An outline names at least one block: an empty file is one empty block.");
|
|
236
|
+
if (blocks.length > MOST_FRAGMENTS)
|
|
237
|
+
throw new ShardError("layout", `An outline of ${blocks.length} blocks is more than the format can number.`);
|
|
238
|
+
let total = 0;
|
|
239
|
+
for (const [place, b] of blocks.entries()) {
|
|
240
|
+
if (b.id.length !== BLOCK_ID_BYTES)
|
|
241
|
+
throw new ShardError("layout", `Block ${place}'s id is ${b.id.length} bytes, not ${BLOCK_ID_BYTES}.`);
|
|
242
|
+
if (!Number.isSafeInteger(b.length) || b.length < 0 || b.length > MOST_BLOCK_BYTES) {
|
|
243
|
+
throw new ShardError("layout", `Block ${place} holds ${b.length} bytes, outside 0 to ${MOST_BLOCK_BYTES}.`);
|
|
244
|
+
}
|
|
245
|
+
if (b.length === 0 && size > 0)
|
|
246
|
+
throw new ShardError("layout", `Block ${place} is empty, and only an empty file has an empty block.`);
|
|
247
|
+
total += b.length;
|
|
248
|
+
}
|
|
249
|
+
if (total !== size)
|
|
250
|
+
throw new ShardError("layout", `The blocks hold ${total} bytes, and the file is ${size}.`);
|
|
251
|
+
}
|
|
252
|
+
/** The bytes the outline's MAC is over: the label, the content id, the size, the count, and each block's id and length in order. */
|
|
253
|
+
function outlineBytes(outline) {
|
|
254
|
+
const head = OUTLINE_LABEL.length + 16 + 8 + 4;
|
|
255
|
+
const out = new Uint8Array(head + outline.blocks.length * (BLOCK_ID_BYTES + 4));
|
|
256
|
+
const view = new DataView(out.buffer);
|
|
257
|
+
out.set(OUTLINE_LABEL, 0);
|
|
258
|
+
out.set(uuidBytes(outline.contentId), OUTLINE_LABEL.length);
|
|
259
|
+
view.setBigUint64(OUTLINE_LABEL.length + 16, BigInt(outline.size), false);
|
|
260
|
+
view.setUint32(OUTLINE_LABEL.length + 24, outline.blocks.length, false);
|
|
261
|
+
for (const [i, b] of outline.blocks.entries()) {
|
|
262
|
+
const at = head + i * (BLOCK_ID_BYTES + 4);
|
|
263
|
+
out.set(b.id, at);
|
|
264
|
+
view.setUint32(at + BLOCK_ID_BYTES, b.length, false);
|
|
265
|
+
}
|
|
266
|
+
return out;
|
|
267
|
+
}
|
|
268
|
+
/** The outline's MAC, which the writer sends with the upload's completion and every reader checks before it releases a byte. */
|
|
269
|
+
export async function outlineMac(keys, outline) {
|
|
270
|
+
checkShape(outline);
|
|
271
|
+
return new Uint8Array(await crypto.subtle.sign("HMAC", keys.outline, outlineBytes(outline)));
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Refuse an outline unless its MAC holds under the file's key: whoever has
|
|
275
|
+
* the map and not the key cannot reorder, drop, repeat or swap a content's
|
|
276
|
+
* blocks, or give one content's blocks as another's.
|
|
277
|
+
*/
|
|
278
|
+
export async function checkOutline(keys, outline, mac) {
|
|
279
|
+
checkShape(outline);
|
|
280
|
+
if (!(await crypto.subtle.verify("HMAC", keys.outline, new Uint8Array(mac), outlineBytes(outline)))) {
|
|
281
|
+
throw new ShardError("outline", "The file's outline is not what was written for it: its blocks have been reordered, dropped or swapped, or it is another file's.");
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function hexOf(b) {
|
|
285
|
+
return Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
|
|
286
|
+
}
|
|
131
287
|
function uuidBytes(id) {
|
|
132
288
|
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id)) {
|
|
133
289
|
throw new RangeError(`A content id is a UUID, and ${JSON.stringify(id)} is not one.`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@filefreak.io/sdk",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "FileFreak's developer API for Node and the browser: every endpoint, uploads straight to storage in parts, downloads in ranges, and the changes to follow.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://filefreak.io/developers/reference",
|