diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..73f69e0
--- /dev/null
+++ b/.idea/.gitignore
@@ -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/
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..03d9549
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..639900d
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..dd06403
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/multicrypt.iml b/.idea/multicrypt.iml
new file mode 100644
index 0000000..d6ebd48
--- /dev/null
+++ b/.idea/multicrypt.iml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml
new file mode 100644
index 0000000..797acea
--- /dev/null
+++ b/.idea/runConfigurations.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 035e255..286cac8 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,3 @@
-# template-npm-typescript
+# multicrypt
-A template repository for NPM packages built with Typescript and PNPM.
\ No newline at end of file
+Multicrypt is a library for multi-key reversible encryption.
diff --git a/package.json b/package.json
index 8beff3f..cc0e7e5 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
- "name": "template-npm-typescript",
+ "name": "multicrypt",
"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",
"types": "lib/index.d.ts",
"directories": {
@@ -21,7 +21,7 @@
"postversion": "git push && git push --tags",
"repository": {
"type": "git",
- "url": "https://code.garrettmills.dev/garrettmills/template-npm-typescript"
+ "url": "https://code.garrettmills.dev/garrettmills/multicrypt"
},
"author": "Garrett Mills ",
"license": "MIT",
diff --git a/src/KeyPackage.ts b/src/KeyPackage.ts
new file mode 100644
index 0000000..7aac29a
--- /dev/null
+++ b/src/KeyPackage.ts
@@ -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 {
+ // 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 {
+ 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(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 {
+ // 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,
+ }
+ }
+}
diff --git a/src/Payload.ts b/src/Payload.ts
new file mode 100644
index 0000000..f448c77
--- /dev/null
+++ b/src/Payload.ts
@@ -0,0 +1,22 @@
+import {EncodedPayload} from './types'
+
+/**
+ * Wrapper class for managing values encoded in a string format.
+ */
+export class Payload {
+ /** Instantiate the payload from a given string. */
+ public static from(v: EncodedPayload): Payload {
+ const encoded = v.slice('multicrypt:'.length)
+ return new Payload(JSON.parse(encoded))
+ }
+
+ /** Instantiate the payload from a given value. */
+ constructor(
+ public readonly value: T,
+ ) {}
+
+ /** Serialize the value. */
+ encode(): EncodedPayload {
+ return `multicrypt:${JSON.stringify(this.value)}`
+ }
+}
diff --git a/src/SharedValue.ts b/src/SharedValue.ts
new file mode 100644
index 0000000..a6fd355
--- /dev/null
+++ b/src/SharedValue.ts
@@ -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 {
+ value: EncodedValue,
+ envelopes: EncodedValue[],
+}
+
+/**
+ * A value that is shared and accessible by multiple encryption keys.
+ */
+export class SharedValue {
+ /** 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(initialSharerKey: KeyValue, value: T): Promise> {
+ const keyPackage = await KeyPackage.fromInitialSharer(initialSharerKey)
+ const cryptor = await keyPackage.getCryptor(initialSharerKey)
+ const payload = new Payload(value)
+
+ const serialized = {
+ value: await cryptor.encode(payload.encode()),
+ ...(keyPackage.toJSON()),
+ }
+
+ return new SharedValue(serialized)
+ }
+
+ constructor(
+ /** The serialized data for this shared value. */
+ private serialized: SerializedSharedValue,
+ 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> {
+ const cryptor = await this.package.getCryptor(key)
+ const decodedValue = await cryptor.decode(this.serialized.value)
+ return Payload.from(decodedValue)
+ }
+
+ /**
+ * Get the value of this secret.
+ * @param key - valid key to decode the value
+ */
+ async get(key: KeyValue): Promise {
+ 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 {
+ const cryptor = await this.package.getCryptor(key)
+ const payload = new Payload(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 {
+ return this.package.addCryptor(sharerKey, shareeKey)
+ }
+
+ /** Serialize the shared value securely. */
+ toJSON(): SerializedSharedValue {
+ return {
+ value: this.serialized.value,
+ ...(this.package.toJSON()),
+ }
+ }
+}
diff --git a/src/crypt/AES256Cryptor.ts b/src/crypt/AES256Cryptor.ts
new file mode 100644
index 0000000..6fdd5d1
--- /dev/null
+++ b/src/crypt/AES256Cryptor.ts
@@ -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 {
+ 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 {
+ 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()
+ }
+}
diff --git a/src/crypt/Cryptor.ts b/src/crypt/Cryptor.ts
new file mode 100644
index 0000000..83286ff
--- /dev/null
+++ b/src/crypt/Cryptor.ts
@@ -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(ctor) ) {
+ throw new Error('Cannot create factory for abstract Cryptor.')
+ }
+
+ this.factoryFunction = key => new (ctor as Instantiable)(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
+
+ /**
+ * Using the key, decode the value.
+ * @param payload
+ */
+ public abstract decode(payload: EncodedValue): Awaitable
+
+ /** Get the key used by this Cryptor. */
+ public getKey(): KeyValue {
+ return this.key
+ }
+}
diff --git a/src/index.ts b/src/index.ts
index acc5e3b..a151bc2 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -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'
diff --git a/src/interface.ts b/src/interface.ts
new file mode 100644
index 0000000..25f4bef
--- /dev/null
+++ b/src/interface.ts
@@ -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 }
diff --git a/src/types.ts b/src/types.ts
new file mode 100644
index 0000000..36f8641
--- /dev/null
+++ b/src/types.ts
@@ -0,0 +1,50 @@
+
+/** Alias for a value that might come in a Promise. */
+export type Awaitable = T | Promise
+
+/** 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 {
+ new(...args: any[]): T
+}
+
+/**
+ * Returns true if the given value is instantiable.
+ * @param what
+ */
+export function isInstantiable(what: unknown): what is Instantiable {
+ 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 = string
+
+/**
+ * Determines that a given item is a valid serialized payload.
+ * @param what
+ */
+export function isEncodedPayload(what: unknown): what is EncodedPayload {
+ if ( typeof what !== 'string' ) {
+ return false
+ }
+
+ return what.startsWith('multicrypt:')
+}