generated from garrettmills/template-npm-typescript
Initial implementation
This commit is contained in:
parent
d20132c63e
commit
5131132c4d
8
.idea/.gitignore
vendored
Normal file
8
.idea/.gitignore
vendored
Normal 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/
|
6
.idea/inspectionProfiles/Project_Default.xml
Normal file
6
.idea/inspectionProfiles/Project_Default.xml
Normal 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
6
.idea/misc.xml
Normal 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
8
.idea/modules.xml
Normal 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
9
.idea/multicrypt.iml
Normal 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>
|
10
.idea/runConfigurations.xml
Normal file
10
.idea/runConfigurations.xml
Normal 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
6
.idea/vcs.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
@ -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.
|
||||
|
@ -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 <shout@garrettmills.dev>",
|
||||
"license": "MIT",
|
||||
|
83
src/KeyPackage.ts
Normal file
83
src/KeyPackage.ts
Normal 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
22
src/Payload.ts
Normal 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
96
src/SharedValue.ts
Normal 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()),
|
||||
}
|
||||
}
|
||||
}
|
35
src/crypt/AES256Cryptor.ts
Normal file
35
src/crypt/AES256Cryptor.ts
Normal 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
62
src/crypt/Cryptor.ts
Normal 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
|
||||
}
|
||||
}
|
@ -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
11
src/interface.ts
Normal 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
50
src/types.ts
Normal 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:')
|
||||
}
|
Loading…
Reference in New Issue
Block a user