Skip to content

Commit

Permalink
TypeScript: rely on new web-stream-tools types, fix SignOptions (#…
Browse files Browse the repository at this point in the history
…1502)

The updated stream types improve type inference and checks, in particular when
using ReadableStreams.

Also:
- add `EncryptSessionKeyOptions` to make it easier to declare wrapper functions
of `encryptSessionKey`;
- tighter output type inference in `Message.getText()` and `.getLiteralData()`.
  • Loading branch information
larabr committed Mar 9, 2022
1 parent a1ef5f5 commit d89cc48
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 48 deletions.
46 changes: 22 additions & 24 deletions openpgp.d.ts
Expand Up @@ -7,6 +7,8 @@
* - Errietta Kostala <https://github.com/errietta>
*/

import type { WebStream as GenericWebStream, NodeStream as GenericNodeStream } from '@openpgp/web-stream-tools';

/* ############## v5 KEY #################### */
// The Key and PublicKey types can be used interchangably since TS cannot detect the difference, as they have the same class properties.
// The declared readKey(s) return type is Key instead of a PublicKey since it seems more obvious that a Key can be cast to a PrivateKey.
Expand Down Expand Up @@ -171,15 +173,9 @@ export class CleartextMessage {

/* ############## v5 MSG #################### */
export function generateSessionKey(options: { encryptionKeys: MaybeArray<PublicKey>, date?: Date, encryptionUserIDs?: MaybeArray<UserID>, config?: PartialConfig }): Promise<SessionKey>;
export function encryptSessionKey(options: SessionKey & {
encryptionKeys?: MaybeArray<PublicKey>, passwords?: MaybeArray<string>, format?: 'armored', wildcard?: boolean, encryptionKeyIDs?: MaybeArray<KeyID>, date?: Date, encryptionUserIDs?: MaybeArray<UserID>, config?: PartialConfig
}) : Promise<string>;
export function encryptSessionKey(options: SessionKey & {
encryptionKeys?: MaybeArray<PublicKey>, passwords?: MaybeArray<string>, format: 'binary', wildcard?: boolean, encryptionKeyIDs?: MaybeArray<KeyID>, date?: Date, encryptionUserIDs?: MaybeArray<UserID>, config?: PartialConfig
}) : Promise<Uint8Array>;
export function encryptSessionKey(options: SessionKey & {
encryptionKeys?: MaybeArray<PublicKey>, passwords?: MaybeArray<string>, format: 'object', wildcard?: boolean, encryptionKeyIDs?: MaybeArray<KeyID>, date?: Date, encryptionUserIDs?: MaybeArray<UserID>, config?: PartialConfig
}) : Promise<Message<Data>>;
export function encryptSessionKey(options: EncryptSessionKeyOptions & { format?: 'armored' }): Promise<string>;
export function encryptSessionKey(options: EncryptSessionKeyOptions & { format: 'binary' }): Promise<Uint8Array>;
export function encryptSessionKey(options: EncryptSessionKeyOptions & { format: 'object' }): Promise<Message<Data>>;
export function decryptSessionKeys<T extends MaybeStream<Data>>(options: { message: Message<T>, decryptionKeys?: MaybeArray<PrivateKey>, passwords?: MaybeArray<string>, date?: Date, config?: PartialConfig }): Promise<SessionKey[]>;

export function readMessage<T extends MaybeStream<string>>(options: { armoredMessage: T, config?: PartialConfig }): Promise<Message<T>>;
Expand Down Expand Up @@ -271,15 +267,15 @@ export class Message<T extends MaybeStream<Data>> {

/** Get literal data that is the body of the message
*/
public getLiteralData(): MaybeStream<Uint8Array> | null;
public getLiteralData(): (T extends Stream<Data> ? WebStream<Uint8Array> : Uint8Array) | null;

/** Returns the key IDs of the keys that signed the message
*/
public getSigningKeyIDs(): KeyID[];

/** Get literal data as text
*/
public getText(): MaybeStream<string> | null;
public getText(): (T extends Stream<Data> ? WebStream<string> : string) | null;

public getFilename(): string | null;

Expand Down Expand Up @@ -548,18 +544,10 @@ export class PacketList<T extends AnyPacket> extends Array<T> {
/* ############## v5 STREAM #################### */

type Data = Uint8Array | string;
interface BaseStream<T extends Data> extends AsyncIterable<T> { }
interface WebStream<T extends Data> extends BaseStream<T> { // copied+simplified version of ReadableStream from lib.dom.d.ts
readonly locked: boolean; getReader: Function; pipeThrough: Function; pipeTo: Function; tee: Function;
cancel(reason?: any): Promise<void>;
}
interface NodeStream<T extends Data> extends BaseStream<T> { // copied+simplified version of ReadableStream from @types/node/index.d.ts
readable: boolean; pipe: Function; unpipe: Function; wrap: Function;
read(size?: number): string | Uint8Array; setEncoding(encoding: string): this; pause(): this; resume(): this;
isPaused(): boolean; unshift(chunk: string | Uint8Array): void;
}
type Stream<T extends Data> = WebStream<T> | NodeStream<T>;
type MaybeStream<T extends Data> = T | Stream<T>;
export interface WebStream<T extends Data> extends GenericWebStream<T> {}
export interface NodeStream<T extends Data> extends GenericNodeStream<T> {}
export type Stream<T extends Data> = WebStream<T> | NodeStream<T>;
export type MaybeStream<T extends Data> = T | Stream<T>;

/* ############## v5 GENERAL #################### */
type MaybeArray<T> = T | Array<T>;
Expand Down Expand Up @@ -627,7 +615,7 @@ interface DecryptOptions {

interface SignOptions {
message: CleartextMessage | Message<MaybeStream<Data>>;
signingKeys?: MaybeArray<PrivateKey>;
signingKeys: MaybeArray<PrivateKey>;
format?: 'armored' | 'binary' | 'object';
detached?: boolean;
signingKeyIDs?: MaybeArray<KeyID>;
Expand All @@ -652,6 +640,16 @@ interface VerifyOptions {
config?: PartialConfig;
}

interface EncryptSessionKeyOptions extends SessionKey {
encryptionKeys?: MaybeArray<PublicKey>,
passwords?: MaybeArray<string>,
format?: 'armored' | 'binary' | 'object',
date?: Date,
wildcard?: boolean,
encryptionKeyIDs?: MaybeArray<KeyID>,
encryptionUserIDs?: MaybeArray<UserID>,
config?: PartialConfig
}

interface SerializedKeyPair<T extends string|Uint8Array> {
privateKey: T;
Expand Down
20 changes: 14 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Expand Up @@ -59,7 +59,7 @@
"@openpgp/pako": "^1.0.12",
"@openpgp/seek-bzip": "^1.0.5-git",
"@openpgp/tweetnacl": "^1.0.3",
"@openpgp/web-stream-tools": "0.0.9",
"@openpgp/web-stream-tools": "^0.0.10",
"@rollup/plugin-commonjs": "^11.1.0",
"@rollup/plugin-node-resolve": "^7.1.3",
"@rollup/plugin-replace": "^2.3.2",
Expand Down Expand Up @@ -87,7 +87,8 @@
"rollup": "^2.38.5",
"rollup-plugin-terser": "^7.0.2",
"sinon": "^4.3.0",
"typescript": "^4.1.2"
"typescript": "^4.1.2",
"web-streams-polyfill": "^3.2.0"
},
"dependencies": {
"asn1.js": "^5.0.0"
Expand Down
52 changes: 36 additions & 16 deletions test/typescript/definitions.ts
Expand Up @@ -5,14 +5,17 @@
* - if it fails to build, edit the file to match type definitions
* - if it fails to run, edit this file to match the actual library API, then edit the definitions file (openpgp.d.ts) accordingly.
*/
import { ReadableStream as WebReadableStream } from 'web-streams-polyfill';
import { createReadStream } from 'fs';

import { expect } from 'chai';
import {
generateKey, readKey, readKeys, readPrivateKey, PrivateKey, Key, PublicKey, revokeKey,
readMessage, createMessage, Message, createCleartextMessage,
encrypt, decrypt, sign, verify, config, enums,
generateSessionKey, encryptSessionKey, decryptSessionKeys,
LiteralDataPacket, PacketList, CompressedDataPacket, PublicKeyPacket, PublicSubkeyPacket, SecretKeyPacket, SecretSubkeyPacket, CleartextMessage
LiteralDataPacket, PacketList, CompressedDataPacket, PublicKeyPacket, PublicSubkeyPacket, SecretKeyPacket, SecretSubkeyPacket, CleartextMessage,
WebStream, NodeStream,
} from '../..';

(async () => {
Expand Down Expand Up @@ -100,8 +103,10 @@ import {
// Get session keys from encrypted message
const sessionKeys = await decryptSessionKeys({ message: await readMessage({ binaryMessage: encryptedBinary }), decryptionKeys: privateKeys });
expect(sessionKeys).to.have.length(1);
const encryptedSessionKeys: string = await encryptSessionKey({ ...sessionKeys[0], passwords: 'pass', algorithm: 'aes128', aeadAlgorithm: 'eax' });
expect(encryptedSessionKeys).to.include('-----BEGIN PGP MESSAGE-----');
const armoredEncryptedSessionKeys: string = await encryptSessionKey({ ...sessionKeys[0], passwords: 'pass', algorithm: 'aes128', aeadAlgorithm: 'eax' });
expect(armoredEncryptedSessionKeys).to.include('-----BEGIN PGP MESSAGE-----');
const encryptedSessionKeys: Message<any> = await encryptSessionKey({ ...sessionKeys[0], passwords: 'pass', algorithm: 'aes128', aeadAlgorithm: 'eax', format: 'object' });
expect(encryptedSessionKeys).to.be.instanceOf(Message);
const newSessionKey = await generateSessionKey({ encryptionKeys: privateKey.toPublic() });
expect(newSessionKey.data).to.exist;
expect(newSessionKey.algorithm).to.exist;
Expand Down Expand Up @@ -180,19 +185,34 @@ import {
// const signed = await sign({ privateKeys, message, detached: true, format: 'binary' });
// console.log(signed); // Uint8Array

// // Streaming - encrypt text message on Node.js (armored)
// const data = fs.createReadStream(filename, { encoding: 'utf8' });
// const message = await createMessage({ text: data });
// const encrypted = await encrypt({ publicKeys, message });
// encrypted.on('data', chunk => {
// console.log(chunk); // String
// });

// // Streaming - encrypt binary message on Node.js (unarmored)
// const data = fs.createReadStream(filename);
// const message = await createMessage({ binary: data });
// const encrypted = await encrypt({ publicKeys, message, format: 'binary' });
// encrypted.pipe(targetStream);
// @ts-expect-error for passing text stream as binary data
await createMessage({ binary: new WebReadableStream<string>() });
// @ts-expect-error for passing binary stream as text data
await createMessage({ text: new WebReadableStream<Uint8Array>() });

// Streaming - encrypt text message (armored output)
try {
const nodeTextStream = createReadStream('non-existent-file', { encoding: 'utf8' });
const messageFromNodeTextStream = await createMessage({ text: nodeTextStream });
(await encrypt({ message: messageFromNodeTextStream, passwords: 'password', format: 'armored' })) as NodeStream<string>;
} catch (err) {}
const webTextStream = new WebReadableStream<string>();
const messageFromWebTextStream = await createMessage({ text: webTextStream });
(await encrypt({ message: messageFromWebTextStream, passwords: 'password', format: 'armored' })) as WebStream<string>;
messageFromWebTextStream.getText() as WebStream<string>;
messageFromWebTextStream.getLiteralData() as WebStream<Uint8Array>;

// Streaming - encrypt binary message (binary output)
try {
const nodeBinaryStream = createReadStream('non-existent-file');
const messageFromNodeBinaryStream = await createMessage({ binary: nodeBinaryStream });
(await encrypt({ message: messageFromNodeBinaryStream, passwords: 'password', format: 'binary' })) as NodeStream<Uint8Array>;
} catch (err) {}
const webBinaryStream = new WebReadableStream<Uint8Array>();
const messageFromWebBinaryStream = await createMessage({ binary: webBinaryStream });
(await encrypt({ message: messageFromWebBinaryStream, passwords: 'password', format: 'binary' })) as WebStream<Uint8Array>;
messageFromWebBinaryStream.getText() as WebStream<string>;
messageFromWebBinaryStream.getLiteralData() as WebStream<Uint8Array>;

console.log('TypeScript definitions are correct');
})().catch(e => {
Expand Down

0 comments on commit d89cc48

Please sign in to comment.