Skip to content

Commit 0770e30

Browse files
skeet70coltfred
andauthored
Add streaming encrypt decrypt (#202)
* Add decryptStream[Unmanaged] * Add streaming encrypt, refactor shared code * Up test coverage to thresholds * Lots of cleanup, DRY, better error handling. * Small fix for tslint * Bump version, add to changelog. * Update copyright * Set up before run * Prettier run * more tests, some comments small code change in decrypt stream * Shift the TransformStream creation into the Frame It's where it's needed, and it makes it so our message types make a little bit more sense and help document what's going on better. * Some code review changes. * More code review changes * Re-add iv check that was dropped with Future refactor --------- Co-authored-by: Colt Frederickson <coltfred@gmail.com>
1 parent 78eb3a6 commit 0770e30

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+2584
-203
lines changed

.prettierignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Ignore everything
2+
**/*
3+
4+
# Un-ignore the file types we want Prettier to format
5+
!**/*.ts
6+
!**/*.tsx
7+
!**/*.js
8+
!**/*.jsx
9+
!**/*.json

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ The IronWeb SDK NPM releases follow standard [Semantic Versioning](https://semve
44

55
**Note:** The patch versions of the IronWeb SDK will not be sequential and might jump by multiple numbers between sequential releases.
66

7+
## v4.3.0
8+
- add streaming encrypt and decrypt functionality for managed and unmanaged documents.
9+
710
## v4.2.49
811
- update `qs` to fix a security vulnerability
912
- switch release process to [Trusted Publishing](https://docs.npmjs.com/trusted-publishers)

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,5 @@ To run a subset of the tests you can use the `-t` option of Jest to only run tes
3939

4040
`yarn run unit GroupCrypto`
4141

42-
Copyright (c) 2022 IronCore Labs, Inc.
42+
Copyright (c) 2026 IronCore Labs, Inc.
4343
All rights reserved.

flake.nix

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@
77
let
88
pkgs = nixpkgs.legacyPackages.${system};
99
in
10-
rec {
10+
{
1111
devShell = pkgs.mkShell {
12-
buildInputs = with pkgs.nodePackages; [
12+
buildInputs = [
13+
pkgs.prettier
1314
pkgs.nodejs_24
1415
(pkgs.yarn.override { nodejs = pkgs.nodejs_24; })
1516
];

ironweb.d.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,22 @@ export interface DocumentAccessResponse {
9696
succeeded: UserOrGroup[];
9797
failed: (UserOrGroup & {error: string})[];
9898
}
99+
export interface StreamEncryptResponse extends DocumentIDNameResponse {
100+
encryptedStream: ReadableStream<Uint8Array>;
101+
}
102+
export interface StreamEncryptUnmanagedResponse {
103+
documentID: string;
104+
encryptedStream: ReadableStream<Uint8Array>;
105+
edeks: Uint8Array;
106+
}
107+
export interface StreamDecryptResponse extends DocumentMetaResponse {
108+
plaintextStream: ReadableStream<Uint8Array>;
109+
}
110+
export interface StreamDecryptUnmanagedResponse {
111+
documentID: string;
112+
plaintextStream: ReadableStream<Uint8Array>;
113+
accessVia: UserOrGroup;
114+
}
99115

100116
/**
101117
* Group SDK response types
@@ -191,8 +207,10 @@ export interface Document {
191207
getMetadata(documentID: string): Promise<DocumentMetaResponse>;
192208
getDocumentIDFromBytes(encryptedDocument: Uint8Array): Promise<string | null>;
193209
decrypt(documentID: string, encryptedDocument: Uint8Array): Promise<DecryptedDocumentResponse>;
210+
decryptStream(documentID: string, encryptedStream: ReadableStream<Uint8Array>): Promise<StreamDecryptResponse>;
194211
decryptFromStore(documentID: string): Promise<DecryptedDocumentResponse>;
195212
encrypt(documentData: Uint8Array, options?: DocumentCreateOptions): Promise<EncryptedDocumentResponse>;
213+
encryptStream(plaintextStream: ReadableStream<Uint8Array>, options?: DocumentCreateOptions): Promise<StreamEncryptResponse>;
196214
encryptToStore(documentData: Uint8Array, options?: DocumentCreateOptions): Promise<DocumentIDNameResponse>;
197215
updateEncryptedData(documentID: string, newDocumentData: Uint8Array): Promise<EncryptedDocumentResponse>;
198216
updateEncryptedDataInStore(documentID: string, newDocumentData: Uint8Array): Promise<DocumentIDNameResponse>;
@@ -201,7 +219,12 @@ export interface Document {
201219
revokeAccess(documentID: string, revokeList: DocumentAccessList): Promise<DocumentAccessResponse>;
202220
advanced: {
203221
decryptUnmanaged(data: Uint8Array, edeks: Uint8Array): Promise<DecryptedUnmanagedDocumentResponse>;
222+
decryptStreamUnmanaged(encryptedStream: ReadableStream<Uint8Array>, edeks: Uint8Array): Promise<StreamDecryptUnmanagedResponse>;
204223
encryptUnmanaged(documentData: Uint8Array, options?: Omit<DocumentCreateOptions, "documentName">): Promise<EncryptedUnmanagedDocumentResponse>;
224+
encryptStreamUnmanaged(
225+
plaintextStream: ReadableStream<Uint8Array>,
226+
options?: Omit<DocumentCreateOptions, "documentName">
227+
): Promise<StreamEncryptUnmanagedResponse>;
205228
};
206229
}
207230

@@ -297,6 +320,8 @@ export interface ErrorCodes {
297320
DOCUMENT_CREATE_WITH_ACCESS_FAILURE: 311;
298321
DOCUMENT_HEADER_PARSE_FAILURE: 312;
299322
DOCUMENT_TRANSFORM_REQUEST_FAILURE: 313;
323+
DOCUMENT_STREAM_DECRYPT_FAILURE: 314;
324+
DOCUMENT_STREAM_ENCRYPT_FAILURE: 315;
300325
GROUP_LIST_REQUEST_FAILURE: 400;
301326
GROUP_GET_REQUEST_FAILURE: 401;
302327
GROUP_CREATE_REQUEST_FAILURE: 402;

nightwatch.json

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,34 @@
11
{
2-
"src_folders": ["integration/nightwatch/tests/"],
3-
"output_folder": "./bin/nightwatch",
4-
"custom_assertions_path": "",
5-
"page_objects_path": "./integration/nightwatch/pageObjects",
6-
"globals_path": "./integration/nightwatch/globalsModule.js",
2+
"src_folders": ["integration/nightwatch/tests/"],
3+
"output_folder": "./bin/nightwatch",
4+
"custom_assertions_path": "",
5+
"page_objects_path": "./integration/nightwatch/pageObjects",
6+
"globals_path": "./integration/nightwatch/globalsModule.js",
77

8-
"selenium": {
9-
"start_process": false
10-
},
11-
12-
"test_settings": {
13-
"default": {
14-
"launch_url": "https://dev1.scrambledbits.org:4500",
15-
"selenium_port": 9515,
16-
"selenium_host": "localhost",
17-
"default_path_prefix" : "",
18-
"desiredCapabilities": {
19-
"browserName": "chrome",
20-
"comment": "'window-size=1920,1080', 'headless', 'disable-dev-shm-usage'",
21-
"chromeOptions": {
22-
"args": ["no-sandbox", "window-size=1920,1080", "headless", "disable-dev-shm-usage"]
23-
},
24-
"acceptSslCerts": true
25-
}
8+
"selenium": {
9+
"start_process": false
2610
},
2711

28-
"chrome": {
29-
"desiredCapabilities": {
30-
"browserName": "chrome"
31-
}
12+
"test_settings": {
13+
"default": {
14+
"launch_url": "https://dev1.scrambledbits.org:4500",
15+
"selenium_port": 9515,
16+
"selenium_host": "localhost",
17+
"default_path_prefix": "",
18+
"desiredCapabilities": {
19+
"browserName": "chrome",
20+
"comment": "'window-size=1920,1080', 'headless', 'disable-dev-shm-usage'",
21+
"chromeOptions": {
22+
"args": ["no-sandbox", "window-size=1920,1080", "headless", "disable-dev-shm-usage"]
23+
},
24+
"acceptSslCerts": true
25+
}
26+
},
27+
28+
"chrome": {
29+
"desiredCapabilities": {
30+
"browserName": "chrome"
31+
}
32+
}
3233
}
33-
}
34-
}
34+
}

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"license": "AGPL-3.0-only",
3-
"version": "4.2.56",
3+
"version": "4.3.0",
44
"scripts": {
55
"cleanTest": "find dist -type d -name tests -prune -exec rm -rf {} \\;",
66
"lint": "eslint . --ext .ts,.tsx",
@@ -16,6 +16,7 @@
1616
},
1717
"dependencies": {
1818
"@ironcorelabs/recrypt-wasm-binding": "0.7.1",
19+
"@noble/ciphers": "1.2.1",
1920
"@stablelib/ed25519": "1.0.2",
2021
"@stablelib/utf8": "1.0.1",
2122
"base64-js": "1.5.1",
@@ -73,4 +74,4 @@
7374
"jsxBracketSameLine": true,
7475
"arrowParens": "always"
7576
}
76-
}
77+
}

src/Constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ export enum ErrorCodes {
6363
DOCUMENT_CREATE_WITH_ACCESS_FAILURE = 311,
6464
DOCUMENT_HEADER_PARSE_FAILURE = 312,
6565
DOCUMENT_TRANSFORM_REQUEST_FAILURE = 313,
66+
DOCUMENT_STREAM_DECRYPT_FAILURE = 314,
67+
DOCUMENT_STREAM_ENCRYPT_FAILURE = 315,
6668
GROUP_LIST_REQUEST_FAILURE = 400,
6769
GROUP_GET_REQUEST_FAILURE = 401,
6870
GROUP_CREATE_REQUEST_FAILURE = 402,

src/FrameMessageTypes.d.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,80 @@ export interface SearchTransliterateStringResponse {
507507
message: string;
508508
}
509509

510+
/* Streaming Decrypt */
511+
export interface DocumentStreamDecryptRequest {
512+
type: "DOCUMENT_STREAM_DECRYPT";
513+
message: {
514+
documentID: string;
515+
iv: Uint8Array;
516+
encryptedStream: ReadableStream<Uint8Array>;
517+
};
518+
}
519+
export interface DocumentStreamDecryptResponse {
520+
type: "DOCUMENT_STREAM_DECRYPT_RESPONSE";
521+
message: Omit<DocumentMetaResponse, "documentID"> & {plaintextStream: ReadableStream<Uint8Array>};
522+
}
523+
524+
export interface DocumentUnmanagedStreamDecryptRequest {
525+
type: "DOCUMENT_UNMANAGED_STREAM_DECRYPT";
526+
message: {
527+
edeks: Uint8Array;
528+
iv: Uint8Array;
529+
encryptedStream: ReadableStream<Uint8Array>;
530+
};
531+
}
532+
export interface DocumentUnmanagedStreamDecryptResponse {
533+
type: "DOCUMENT_UNMANAGED_STREAM_DECRYPT_RESPONSE";
534+
message: {
535+
accessVia: UserOrGroup;
536+
plaintextStream: ReadableStream<Uint8Array>;
537+
};
538+
}
539+
540+
/* Streaming Encrypt */
541+
export interface DocumentStreamEncryptRequest {
542+
type: "DOCUMENT_STREAM_ENCRYPT";
543+
message: {
544+
documentID: string;
545+
documentName: string;
546+
plaintextStream: ReadableStream<Uint8Array>;
547+
userGrants: string[];
548+
groupGrants: string[];
549+
grantToAuthor: boolean;
550+
policy?: Policy;
551+
};
552+
}
553+
export interface DocumentStreamEncryptResponse {
554+
type: "DOCUMENT_STREAM_ENCRYPT_RESPONSE";
555+
message: {
556+
documentID: string;
557+
documentName: string | null;
558+
encryptedStream: ReadableStream<Uint8Array>;
559+
created: string;
560+
updated: string;
561+
};
562+
}
563+
564+
export interface DocumentUnmanagedStreamEncryptRequest {
565+
type: "DOCUMENT_UNMANAGED_STREAM_ENCRYPT";
566+
message: {
567+
documentID: string;
568+
plaintextStream: ReadableStream<Uint8Array>;
569+
userGrants: string[];
570+
groupGrants: string[];
571+
grantToAuthor: boolean;
572+
policy?: Policy;
573+
};
574+
}
575+
export interface DocumentUnmanagedStreamEncryptResponse {
576+
type: "DOCUMENT_UNMANAGED_STREAM_ENCRYPT_RESPONSE";
577+
message: {
578+
documentID: string;
579+
edeks: Uint8Array;
580+
encryptedStream: ReadableStream<Uint8Array>;
581+
};
582+
}
583+
510584
export interface ErrorResponse {
511585
type: "ERROR_RESPONSE";
512586
message: {
@@ -531,6 +605,10 @@ export type RequestMessage =
531605
| DocumentStoreEncryptRequest
532606
| DocumentEncryptRequest
533607
| DocumentStoreUpdateDataRequest
608+
| DocumentStreamDecryptRequest
609+
| DocumentUnmanagedStreamDecryptRequest
610+
| DocumentStreamEncryptRequest
611+
| DocumentUnmanagedStreamEncryptRequest
534612
| DocumentUpdateDataRequest
535613
| DocumentUpdateNameRequest
536614
| DocumentGrantRequest
@@ -571,6 +649,10 @@ export type ResponseMessage =
571649
| DocumentStoreEncryptResponse
572650
| DocumentEncryptResponse
573651
| DocumentStoreUpdateDataResponse
652+
| DocumentStreamDecryptResponse
653+
| DocumentUnmanagedStreamDecryptResponse
654+
| DocumentStreamEncryptResponse
655+
| DocumentUnmanagedStreamEncryptResponse
574656
| DocumentUpdateDataResponse
575657
| DocumentUpdateNameResponse
576658
| DocumentGrantResponse

src/WorkerMessageTypes.d.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,43 @@ export interface SearchTransliterateStringResponse {
289289
message: string;
290290
}
291291

292+
export interface StreamDecryptDocumentWorkerRequest {
293+
type: "DOCUMENT_STREAM_DECRYPT";
294+
message: {
295+
encryptedSymmetricKey: TransformedEncryptedMessage;
296+
privateKey: PrivateKey<Uint8Array>;
297+
iv: Uint8Array;
298+
encryptedStream: ReadableStream<Uint8Array>;
299+
plaintextStream: WritableStream<Uint8Array>;
300+
};
301+
}
302+
export interface StreamDecryptDocumentWorkerResponse {
303+
type: "DOCUMENT_STREAM_DECRYPT_RESPONSE";
304+
// There's nothing to send back (all the data already went out in the `plaintextResponse` from the request), but
305+
// this response indicates to the frame that the decrypt work is done. The explicit void is to (try) to communicate
306+
// that we have no data for the frame but we're done.
307+
message: void;
308+
}
309+
310+
export interface StreamEncryptDocumentWorkerRequest {
311+
type: "DOCUMENT_STREAM_ENCRYPT";
312+
message: {
313+
plaintextStream: ReadableStream<Uint8Array>;
314+
ciphertextStream: WritableStream<Uint8Array>;
315+
userKeyList: UserOrGroupPublicKey[];
316+
groupKeyList: UserOrGroupPublicKey[];
317+
signingKeys: SigningKeyPair;
318+
iv: Uint8Array;
319+
};
320+
}
321+
export interface StreamEncryptDocumentWorkerResponse {
322+
type: "DOCUMENT_STREAM_ENCRYPT_RESPONSE";
323+
message: {
324+
userAccessKeys: EncryptedAccessKey[];
325+
groupAccessKeys: EncryptedAccessKey[];
326+
};
327+
}
328+
292329
export interface ErrorResponse {
293330
type: "ERROR_RESPONSE";
294331
message: {
@@ -304,6 +341,8 @@ export type RequestMessage =
304341
| ReencryptDocumentWorkerRequest
305342
| DecryptDocumentWorkerRequest
306343
| DocumentEncryptToKeysWorkerRequest
344+
| StreamDecryptDocumentWorkerRequest
345+
| StreamEncryptDocumentWorkerRequest
307346
| NewUserKeygenWorkerRequest
308347
| NewUserAndDeviceKeygenWorkerRequest
309348
| DeviceKeygenWorkerRequest
@@ -325,6 +364,8 @@ export type ResponseMessage =
325364
| ReencryptDocumentWorkerResponse
326365
| DecryptDocumentWorkerResponse
327366
| DocumentEncryptToKeysWorkerResponse
367+
| StreamDecryptDocumentWorkerResponse
368+
| StreamEncryptDocumentWorkerResponse
328369
| NewUserKeygenWorkerResponse
329370
| NewUserAndDeviceKeygenWorkerResponse
330371
| DeviceKeygenWorkerResponse

0 commit comments

Comments
 (0)