forked from skiff-org/skiff-apps
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request skiff-org#63 from skiff-org/rliu/rms-1684-publish-…
…updated-skiff-crypto-with-v2-merged RMS-1526, RMS-1684: Publish remaining dependencies for skemail-web
- Loading branch information
Showing
452 changed files
with
45,501 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export * from './common'; | ||
export * from './datagramClasses'; | ||
export * from './secretbox'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.