Skip to content

Commit

Permalink
Merge pull request skiff-org#63 from skiff-org/rliu/rms-1684-publish-…
Browse files Browse the repository at this point in the history
…updated-skiff-crypto-with-v2-merged

RMS-1526, RMS-1684: Publish remaining dependencies for skemail-web
  • Loading branch information
rrrliu authored Jun 16, 2023
2 parents 6665f15 + e4bf253 commit ff354ec
Show file tree
Hide file tree
Showing 452 changed files with 45,501 additions and 21 deletions.
3 changes: 3 additions & 0 deletions libs/skiff-crypto/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,18 @@
"typescript": "^5.0.0"
},
"dependencies": {
"@stablelib/base64": "^1.0.1",
"@stablelib/chacha20poly1305": "^1.0.1",
"argon2-browser": "^1.18.0",
"base64-js": "^1.5.1",
"buffer": "^6.0.3",
"fflate": "^0.7.3",
"futoin-hkdf": "^1.5.0",
"lodash": "^4.17.21",
"protobufjs": "^6.11.3",
"randombytes": "^2.1.0",
"semver": "^7.3.4",
"skiff-graphql": "workspace:libs/skiff-graphql",
"tslib": "^2.4.0",
"tweetnacl": "^1.0.3",
"varint": "^6.0.0"
Expand Down
161 changes: 161 additions & 0 deletions libs/skiff-crypto/src/aead-v2/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// This file provides interface definitions for the AEAD library that is used for encryption and decryption
// Skiff data.
//
// Datagram:
// The Datagram interface specifies the minimum set of functions needed to serialize and deserialize a typed and versioned object.
// A Datagram is composed of two parts - a header and a body that are individually typed and can be seperately serialized/deserialized.
// - Body: The datagram body should data intended to be encrypted/decrypted.
// - Header: The datagram header should contain unencrypted data that should be included alongside the encrypted body.
//
// AADMeta:
// The AADMeta structure is used to store the datagram header alonside other metadata necessary for encryption and
// backwards compatibility. The serialized datagram header is stored in the 'header' field of the AADMeta.
//
// Envelope:
// The Envelope interface specifies the set of functions needed to encrypt/decrypt a datagram.
// Currently, Skiff maintains a single general implementation of the Envelope interface suitable for any datagram,
// and located in `secretbox.ts`.

import { Range } from 'semver';

import { concatUint8Arrays, extractVarintPrefixed, varintPrefixed } from '../aead/typedArraysUtils';
import { utf8BytesToString, utf8StringToBytes } from 'src/utf8';

interface Typed {
type: string;
}

interface Versioned {
version: string;
versionConstraint: Range;
}

interface Serializable<Header, Body> {
serializeHeader(data: Header, version: string): Uint8Array;
serializeBody(data: Body, version: string): Uint8Array;
}

interface Deserializable<Header, Body> {
deserializeHeader(bytes: Uint8Array, version: string): Header;
deserializeBody(bytes: Uint8Array, version: string): Body;
}

// Datagram is the minimum set of functions needed to serialize and deserialize a typed and versioned object.
export type DatagramV2<Header, Body> = Typed & Versioned & Serializable<Header, Body> & Deserializable<Header, Body>;
interface EncryptDatagram<Header, Body> {
encrypt(datagram: DatagramV2<Header, Body>, header: Header, data: Body): TypedBytesV2;
}

interface DecryptDatagram<Header, Body> {
decrypt(
datagram: DatagramV2<Header, Body>,
bytes: TypedBytesV2
): {
metadata: AADMetaV2;
header: Header;
body: Body;
};
}

// Envelope is the minimum set of functions needed to encrypt and decrypt a bytestream.
export type EnvelopeV2<Header, Body> = EncryptDatagram<Header, Body> & DecryptDatagram<Header, Body>;

/**
* AADMeta is a class that encapsulates the additional metadata included in these envelope implementations.
*/
const AAD_METADATA_VERSION = '0.2.0';
export interface AADMetaV2 {
datagramVersion: string;
datagramType: string;
nonce: Uint8Array;
rawHeader: Uint8Array;
}

export function deserializeAADMeta(data: TypedBytesV2): {
metadata: AADMetaV2;
rawMetadata: Uint8Array;
content: Uint8Array;
} | null {
const rawBytes = extractVarintPrefixed({ bs: data });
const rawMetadata = varintPrefixed(rawBytes);
const content = data.slice(rawMetadata.length);
const metadataBuf = { bs: rawBytes };
const metadataVersion = utf8BytesToString(extractVarintPrefixed(metadataBuf));
if (metadataVersion !== AAD_METADATA_VERSION) {
throw new Error('unrecognized metadata version');
}
const datagramVersion = utf8BytesToString(extractVarintPrefixed(metadataBuf));
const datagramType = utf8BytesToString(extractVarintPrefixed(metadataBuf));
const nonce = extractVarintPrefixed(metadataBuf);
const rawHeader = extractVarintPrefixed(metadataBuf);

const metadata: AADMetaV2 = {
datagramVersion,
datagramType,
nonce,
rawHeader
};
if (metadataBuf.bs.length !== 0) {
throw new Error('unexpected additional content in header');
}
return {
metadata,
rawMetadata,
content
};
}

export function serializeAADMeta(metadata: AADMetaV2): Uint8Array {
/**
* A serialized AAD metadata object contains five pieces of information:
* version of the metadata format
* version of the encrypted object
* type name of the encrypted object
* nonce used for the encryption scheme
* arbitrary additional serialized metadata, for use by consumers
*
* It is composed of several varint-prefixed Uint8Arrays, which is then itself expressed as a
* varint-prefixed byte array.
*
* It looks like this on the wire:
* NNxxxxxxxxxxxxxxxxxxxxxxxxx...
* AAxx...BBxx...CCxx...DDxx...EExx...
*
* where AA, BB, CC, DD, EE, and NN are varint-encoded and express the number of bytes following
* that indicator which comprise that field.
*
* AAxxx is the prefixed metadata format version
* BBxxx is the prefixed object version
* CCxxx is the prefixed typename
* DDxxx is the prefixed nonce. Length is prefixed instead of static to allow for multiple envelope types.
* EExxx is arbitrary additional header data specified by the datagram.
*
* and NNxxx is the prefixed length of those four strings concatenated together.
*
*/
const data: Uint8Array = concatUint8Arrays(
varintPrefixed(utf8StringToBytes(AAD_METADATA_VERSION)),
varintPrefixed(utf8StringToBytes(metadata.datagramVersion)),
varintPrefixed(utf8StringToBytes(metadata.datagramType)),
varintPrefixed(metadata.nonce),
varintPrefixed(metadata.rawHeader)
);

return varintPrefixed(data);
}

/** TypedBytes is a simple extension of a Uint8Array. It introduces a function that lets us inspect the metadata.
*
* If the content being provided doesn't have the associated metadata, nonsense may be returned.
*/
export class TypedBytesV2 extends Uint8Array {
inspect(): AADMetaV2 | null {
const parsed = deserializeAADMeta(this);

if (parsed == null || parsed.metadata == null) {
return null;
}

return parsed.metadata;
}
}
38 changes: 38 additions & 0 deletions libs/skiff-crypto/src/aead-v2/datagramClasses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/* eslint-disable max-classes-per-file */

import { Range } from 'semver';
import { utf8BytesToString, utf8StringToBytes } from 'skiff-utils';

import { DatagramV2 } from './common';

/**
* Create a datagram that encode and decode any JSON.stringify-able data
* @param {string} type datagram type
* @param {string} version datagram version, default 0.1.0
* @param {Range} versionConstraint datagram version contraint, default 0.1.*
*/
export const createRawJSONDatagramV2 = <Header, Body>(
type: string,
version = '0.1.0',
versionConstraint = new Range('0.1.*')
) => {
if (!versionConstraint.test(version)) {
throw new Error("Provided version constraint doesn't validate provided version");
}
const datagram: DatagramV2<Header, Body> = {
versionConstraint,
version,
type,
serializeBody(data) {
return utf8StringToBytes(JSON.stringify(data));
},
deserializeBody(data) {
return JSON.parse(utf8BytesToString(data)) as Body;
},
serializeHeader(header) {
return utf8StringToBytes(JSON.stringify(header));
},
deserializeHeader: (header) => JSON.parse(utf8BytesToString(header)) as Header
};
return datagram;
};
3 changes: 3 additions & 0 deletions libs/skiff-crypto/src/aead-v2/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './common';
export * from './datagramClasses';
export * from './secretbox';
84 changes: 84 additions & 0 deletions libs/skiff-crypto/src/aead-v2/secretbox.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { randomBytes } from 'crypto';

import { ChaCha20Poly1305, NONCE_LENGTH } from '@stablelib/chacha20poly1305';

import { concatUint8Arrays } from '../aead/typedArraysUtils';

import {
AADMetaV2,
DatagramV2,
deserializeAADMeta as deserializeAADMeta,
EnvelopeV2,
serializeAADMeta as serializeAADMeta,
TypedBytesV2
} from './common';

/**
* TaggedSecretBox is an implementation of nacl.secretbox, but additionally includes the version and type information
* of the encrypted content in the AD headers.
*/
class TaggedSecretBox implements EnvelopeV2<any, any> {
private readonly key: ChaCha20Poly1305;

constructor(keyBytes: Uint8Array) {
this.key = new ChaCha20Poly1305(keyBytes);
}

encrypt<Header, Body>(datagram: DatagramV2<Header, Body>, header: Header, data: Body): TypedBytesV2 {
const nonce = randomBytes(NONCE_LENGTH);
const aad: AADMetaV2 = {
datagramVersion: datagram.version,
datagramType: datagram.type,
nonce,
rawHeader: datagram.serializeHeader(header, datagram.version)
};
const aadSerialized = serializeAADMeta(aad);

return new TypedBytesV2(
concatUint8Arrays(
aadSerialized,
this.key.seal(nonce, datagram.serializeBody(data, datagram.version), aadSerialized)
)
);
}

decrypt<Header, Body>(
datagram: DatagramV2<Header, Body>,
bytes: TypedBytesV2
): { metadata: AADMetaV2; header: Header; body: Body } {
const parsedMetadata = deserializeAADMeta(bytes);
if (parsedMetadata === null || parsedMetadata.metadata === null) {
throw new Error("Couldn't decrypt: no header in provided data");
}
const decrypted: Uint8Array | null = this.key.open(
parsedMetadata.metadata.nonce,
parsedMetadata.content,
parsedMetadata.rawMetadata
);
if (!decrypted) {
throw new Error("Couldn't decrypt: invalid key");
}

if (datagram.type !== parsedMetadata.metadata.datagramType) {
throw new Error(
`Couldn't decrypt: encrypted type (${parsedMetadata.metadata.datagramType}) doesnt match datagram type (${datagram.type})`
);
}

if (!datagram.versionConstraint.test(parsedMetadata.metadata.datagramVersion)) {
throw new Error(
`Couldn't decrypt: encrypted version (${
parsedMetadata.metadata.datagramVersion
}) doesnt match datagram version constraint (${datagram.versionConstraint.format()})`
);
}

return {
metadata: parsedMetadata.metadata,
header: datagram.deserializeHeader(parsedMetadata.metadata.rawHeader, parsedMetadata.metadata.datagramVersion),
body: datagram.deserializeBody(decrypted, parsedMetadata.metadata.datagramVersion)
};
}
}

export default TaggedSecretBox;
2 changes: 1 addition & 1 deletion libs/skiff-crypto/src/asymmetricEncryption.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { fromByteArray, toByteArray } from 'base64-js';
import memoize from 'lodash/memoize';
import { utf8BytesToString, utf8StringToBytes } from './utf8';
import nacl from 'tweetnacl';
import { utf8BytesToString, utf8StringToBytes } from './utf8';

/**
* Generate a nonce for tweetnacl-js.
Expand Down
Loading

0 comments on commit ff354ec

Please sign in to comment.