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