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",
|
"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
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