Initial implementation

This commit is contained in:
Garrett Mills 2021-11-22 12:41:52 -06:00
parent d20132c63e
commit 5131132c4d
17 changed files with 423 additions and 7 deletions

8
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

6
.idea/misc.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/multicrypt.iml" filepath="$PROJECT_DIR$/.idea/multicrypt.iml" />
</modules>
</component>
</project>

9
.idea/multicrypt.iml Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.android.tools.idea.compose.preview.runconfiguration.ComposePreviewRunConfigurationProducer" />
</set>
</option>
</component>
</project>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@ -1,3 +1,3 @@
# template-npm-typescript # multicrypt
A template repository for NPM packages built with Typescript and PNPM. Multicrypt is a library for multi-key reversible encryption.

View File

@ -1,7 +1,7 @@
{ {
"name": "template-npm-typescript", "name": "multicrypt",
"version": "0.1.0", "version": "0.1.0",
"description": "A template for NPM packages built with TypeScript", "description": "A library for multi-key reversible encryption",
"main": "lib/index.js", "main": "lib/index.js",
"types": "lib/index.d.ts", "types": "lib/index.d.ts",
"directories": { "directories": {
@ -21,7 +21,7 @@
"postversion": "git push && git push --tags", "postversion": "git push && git push --tags",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://code.garrettmills.dev/garrettmills/template-npm-typescript" "url": "https://code.garrettmills.dev/garrettmills/multicrypt"
}, },
"author": "Garrett Mills <shout@garrettmills.dev>", "author": "Garrett Mills <shout@garrettmills.dev>",
"license": "MIT", "license": "MIT",

83
src/KeyPackage.ts Normal file
View File

@ -0,0 +1,83 @@
import {EncodedValue, isEncodedPayload, KeyValue} from './types'
import {Cryptor, InvalidKeyError} from './crypt/Cryptor'
import {pkg} from './interface'
import {Payload} from './Payload'
/**
* A collection of keys that are used to decode a base key.
*
* The base key is used to encode various values that can then be accessed
* by all of the other keys in the package.
*/
export class KeyPackage {
/** Create a new key package from an initial sharer's key. */
public static async fromInitialSharer(initialSharerKey: KeyValue): Promise<KeyPackage> {
// Get the cryptor for the initial sharer
const sharerCryptor = pkg.cryptor(initialSharerKey)
// Encode a new global key using the initial sharer's key
const envelope = new Payload(Cryptor.getNewKey())
const envelopeKey = await sharerCryptor.encode(envelope.encode())
// Return a new KeyPackage with the created envelope
return new KeyPackage([envelopeKey])
}
/** Instantiate a key package from its serialized JSON value. */
public static fromJSON(value: { envelopes: EncodedValue[] }): KeyPackage {
return new KeyPackage(value.envelopes)
}
constructor(
/** The encoded user keys. */
private readonly envelopes: EncodedValue[],
) {}
/**
* Get a Cryptor instance for master key given some user's key, if that key is valid.
* @param key
*/
async getCryptor(key: KeyValue): Promise<Cryptor> {
const envelopeCryptor = pkg.cryptor(key)
for ( const encodedKey of this.envelopes ) {
// Attempt to decode one of the envelope keys using the input key
const decodedKey = await envelopeCryptor.decode(encodedKey)
if ( isEncodedPayload(decodedKey) ) {
// We successfully decoded the envelope, which contains the global key
return pkg.cryptor(Payload.from<string>(decodedKey).value)
}
}
throw new InvalidKeyError()
}
/**
* Add a user's key to this package, granting them access to the master key.
* @param sharerKey - a currently valid user's key
* @param shareeKey - the key to add to the package
*/
async addCryptor(sharerKey: KeyValue, shareeKey?: KeyValue): Promise<KeyValue> {
// Validate the sharer's key and get the cryptor
const sharerCryptor = await this.getCryptor(sharerKey)
// Create a new cryptor for the sharee
const determinedShareeKey = shareeKey || Cryptor.getNewKey()
const shareeCryptor = pkg.cryptor(determinedShareeKey)
// Encode the global key using the sharee's key
const envelope = new Payload(sharerCryptor.getKey())
const envelopeKey = await shareeCryptor.encode(envelope.encode())
this.envelopes.push(envelopeKey)
// Return the sharee's key that can be used to decode the global key
return determinedShareeKey
}
/** Serialize the key package securely. */
toJSON() {
return {
envelopes: this.envelopes,
}
}
}

22
src/Payload.ts Normal file
View File

@ -0,0 +1,22 @@
import {EncodedPayload} from './types'
/**
* Wrapper class for managing values encoded in a string format.
*/
export class Payload<T> {
/** Instantiate the payload from a given string. */
public static from<T>(v: EncodedPayload<T>): Payload<T> {
const encoded = v.slice('multicrypt:'.length)
return new Payload<T>(JSON.parse(encoded))
}
/** Instantiate the payload from a given value. */
constructor(
public readonly value: T,
) {}
/** Serialize the value. */
encode(): EncodedPayload<T> {
return `multicrypt:${JSON.stringify(this.value)}`
}
}

96
src/SharedValue.ts Normal file
View File

@ -0,0 +1,96 @@
import {EncodedValue, KeyValue} from './types'
import {KeyPackage} from './KeyPackage'
import {Payload} from './Payload'
/** Interface for a JSON-serializable shared value. */
export interface SerializedSharedValue<T> {
value: EncodedValue,
envelopes: EncodedValue[],
}
/**
* A value that is shared and accessible by multiple encryption keys.
*/
export class SharedValue<T> {
/** The package of keys allowed to access the encoded value. */
public readonly package: KeyPackage
/**
* Create a new shared value.
* @param initialSharerKey - an initial key, used to give access to the encoded value
* @param value - the value to be encoded and shared
*/
public static async create<T>(initialSharerKey: KeyValue, value: T): Promise<SharedValue<T>> {
const keyPackage = await KeyPackage.fromInitialSharer(initialSharerKey)
const cryptor = await keyPackage.getCryptor(initialSharerKey)
const payload = new Payload<T>(value)
const serialized = {
value: await cryptor.encode(payload.encode()),
...(keyPackage.toJSON()),
}
return new SharedValue<T>(serialized)
}
constructor(
/** The serialized data for this shared value. */
private serialized: SerializedSharedValue<T>,
public readonly validator: (w: unknown) => w is T = (w: unknown): w is T => true,
) {
this.package = KeyPackage.fromJSON(serialized)
}
/**
* Get a Payload instance of the decoded value.
* @param key - valid key to decode the payload
*/
async payload(key: KeyValue): Promise<Payload<T>> {
const cryptor = await this.package.getCryptor(key)
const decodedValue = await cryptor.decode(this.serialized.value)
return Payload.from<T>(decodedValue)
}
/**
* Get the value of this secret.
* @param key - valid key to decode the value
*/
async get(key: KeyValue): Promise<T> {
const payload = await this.payload(key)
const value = payload.value
if ( !this.validator(value) ) {
throw new TypeError('Invalid encoded value!')
}
return value
}
/**
* Set a new value for this secret.
* @param key - valid key to encode the value
* @param value - new value to encode
*/
async set(key: KeyValue, value: T): Promise<void> {
const cryptor = await this.package.getCryptor(key)
const payload = new Payload<T>(value)
this.serialized.value = await cryptor.encode(payload.encode())
}
/**
* Add a new key to the key package for this shared value. This allows
* the `shareeKey` to access this shared value.
* @param sharerKey - a currently valid key
* @param shareeKey - the new key to add
*/
async addKey(sharerKey: KeyValue, shareeKey?: KeyValue): Promise<KeyValue> {
return this.package.addCryptor(sharerKey, shareeKey)
}
/** Serialize the shared value securely. */
toJSON(): SerializedSharedValue<T> {
return {
value: this.serialized.value,
...(this.package.toJSON()),
}
}
}

View File

@ -0,0 +1,35 @@
import {Cryptor} from './Cryptor'
import {Awaitable, DecodedValue, EncodedValue} from '../types'
import * as crypto from 'crypto'
import { Buffer } from 'buffer'
/**
* AES 256 implementation of a Cryptor.
*/
export class AES256Cryptor extends Cryptor {
encode(value: DecodedValue): Awaitable<EncodedValue> {
const hash = crypto.createHash('sha256')
hash.update(this.key)
const vector = crypto.randomBytes(16)
const cipher = crypto.createCipheriv('aes-256-ctr', hash.digest(), vector)
const input = Buffer.from(value)
const cipherText = cipher.update(input)
const encoded = Buffer.concat([vector, cipherText, cipher.final()])
return encoded.toString('base64')
}
decode(payload: EncodedValue): Awaitable<DecodedValue> {
const hash = crypto.createHash('sha256')
hash.update(this.key)
const input = Buffer.from(payload, 'base64')
const vector = input.slice(0, 16)
const decipher = crypto.createDecipheriv('aes-256-ctr', hash.digest(), vector)
const cipherText = input.slice(16)
const decoded = Buffer.concat([decipher.update(cipherText), decipher.final()])
return decoded.toString()
}
}

62
src/crypt/Cryptor.ts Normal file
View File

@ -0,0 +1,62 @@
import {Awaitable, DecodedValue, EncodedValue, Instantiable, isInstantiable, KeyValue} from '../types'
import * as uuid from 'uuid'
/**
* Error thrown when a key fails to decode an encoded value.
*/
export class InvalidKeyError extends Error {
constructor() {
super('The provided key is invalid and cannot be used to decode the payload.')
}
}
/**
* Base class for implementations that encode and decode values using some key.
*/
export abstract class Cryptor {
/** Function used to instantiate the Cryptor. */
protected static factoryFunction?: (key: KeyValue) => Cryptor
/** Generate a new, random encryption key. */
public static getNewKey(): string {
return uuid.v4().replace(/-/g, '')
}
/**
* Get a factory that produces an instance of this cryptor.
*/
public static getFactory(): (key: KeyValue) => Cryptor {
if ( !this.factoryFunction ) {
const ctor = this as typeof Cryptor
if ( !isInstantiable<Cryptor>(ctor) ) {
throw new Error('Cannot create factory for abstract Cryptor.')
}
this.factoryFunction = key => new (ctor as Instantiable<Cryptor>)(key)
}
return this.factoryFunction
}
constructor(
/** The key used by this cryptor. */
protected key: KeyValue,
) {}
/**
* Using the key, encode the value.
* @param value
*/
public abstract encode(value: DecodedValue): Awaitable<EncodedValue>
/**
* Using the key, decode the value.
* @param payload
*/
public abstract decode(payload: EncodedValue): Awaitable<DecodedValue>
/** Get the key used by this Cryptor. */
public getKey(): KeyValue {
return this.key
}
}

View File

@ -1,3 +1,7 @@
export const HELLO_WORLD = 'Hello, World!' export * from './types'
export * from './crypt/Cryptor'
export * from './crypt/AES256Cryptor'
export * from './KeyPackage'
export * from './Payload'
export * from './SharedValue'

11
src/interface.ts Normal file
View File

@ -0,0 +1,11 @@
import {AES256Cryptor} from './crypt/AES256Cryptor'
/**
* Set of values that can be overridden externally to modify the behavior of this package.
*/
const pkg = {
/** Factory for producing the default Cryptor instance. */
cryptor: AES256Cryptor.getFactory(),
}
export { pkg }

50
src/types.ts Normal file
View File

@ -0,0 +1,50 @@
/** Alias for a value that might come in a Promise. */
export type Awaitable<T> = T | Promise<T>
/** String that has been decrypted. */
export type DecodedValue = string
/** String that has been encrypted. */
export type EncodedValue = string
/** A key used for encryption. */
export type KeyValue = string
/**
* Interface that designates a particular value as able to be constructed.
*/
export interface Instantiable<T> {
new(...args: any[]): T
}
/**
* Returns true if the given value is instantiable.
* @param what
*/
export function isInstantiable<T>(what: unknown): what is Instantiable<T> {
return (
Boolean(what)
&& (typeof what === 'object' || typeof what === 'function')
&& (what !== null)
&& 'constructor' in what
&& typeof what.constructor === 'function'
)
}
/**
* Serialized payload of a given value.
*/
export type EncodedPayload<T> = string
/**
* Determines that a given item is a valid serialized payload.
* @param what
*/
export function isEncodedPayload(what: unknown): what is EncodedPayload<unknown> {
if ( typeof what !== 'string' ) {
return false
}
return what.startsWith('multicrypt:')
}