UNPKG

freehold

Version:

App data encryption for web3

207 lines (177 loc) 6.26 kB
import { hexlify } from '@ethersproject/bytes'; import { toUtf8Bytes } from '@ethersproject/strings'; import { ethers } from 'ethers'; import webcrypto from "webcrypto-liner"; let crypto:typeof webcrypto.nativeCrypto; if (typeof webcrypto === "undefined") { crypto = require("crypto").webcrypto; } export type SignatureType = "message" | "password"; export type SignatureFragment = { data: string, type: SignatureType } type EncryptMethod = (plaintext:string) => Promise<string>; type DecryptMethod = (cipherText:string) => Promise<string>; class FreeholdSigner { private _encrypt:EncryptMethod; private _decrypt:DecryptMethod; constructor( encrypt: EncryptMethod, decrypt: DecryptMethod ) { this._encrypt = encrypt; this._decrypt = decrypt; } async encrypt(plaintext:string) { return this._encrypt(plaintext); } async decrypt(cipherText:string) { return this._decrypt(cipherText); } } async function __createSignerObject(secretKey:string) { // Structure of this encryption is a mixture of the following two resources: // https://gist.github.com/chrisveness/43bcda93af9f646d083fad678071b90a // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/deriveKey#pbkdf2_2 let enc = new TextEncoder(); let seed = Buffer.concat([ enc.encode(secretKey), //crypto.getRandomValues(new Uint8Array(16)) ]) let masterKey = await crypto.subtle.importKey( "raw", seed, "PBKDF2", false, ["deriveBits", "deriveKey"] ) // Just for good measure. Keep the raw data out of scope. secretKey = undefined; return new FreeholdSigner( async (plaintext:string) => { // Generate a random 96-bit IV for use in encryption/decryption // and a 128-bit salt. const iv = crypto.getRandomValues(new Uint8Array(12)); const salt = crypto.getRandomValues(new Uint8Array(16)); // Create a PBKDF2 key based on the salt and the master key let key = await crypto.subtle.deriveKey( { name: "PBKDF2", salt: salt, iterations: 100000, hash: "SHA-256" }, masterKey, {name: "AES-GCM", length: 256}, false, ["encrypt"] ) // Encrypt the plaintext via AES using the key and the iv const ct = await crypto.subtle.encrypt( { name: "AES-GCM", iv: iv }, key, enc.encode(plaintext) ) // Convert result to buffer const ctBuffer = Buffer.from(ct); // Return a base64url encoded string that includes both the iv, salt, and // cipher text. This makes the data easy to store and pass around. return Buffer.concat([ iv, salt, ctBuffer ]).toString("base64url"); }, async (cipherText:string) => { // Convert from base64url to buffer const data = Buffer.from(cipherText, "base64url"); // Grab the iv and salt from the beginning of the data buffer const iv = data.subarray(0, 12); const salt = data.subarray(12, 28); // Recreate the deterministic key from the salt and the master key let key = await crypto.subtle.deriveKey( { name: "PBKDF2", salt: salt, iterations: 100000, hash: "SHA-256" }, masterKey, {name: "AES-GCM", length: 256}, false, ["decrypt"] ) // Get the remaining bytes of the cipher text as the encrypted data const ciphertext = data.subarray(28); try { // Decrypt the cipher text using the key. const plainBuffer = await crypto.subtle.decrypt( { name: 'AES-GCM', iv: iv }, key, ciphertext ) // And convert back to string. return new TextDecoder().decode(plainBuffer); } catch (e) { throw new Error('Decrypt failed: ' + typeof e.message !== "undefined" ? e.message : e); } } ) } async function __createSigner( provider:ethers.providers.JsonRpcProvider, address:string, fragmentData:string):Promise<FreeholdSigner> { return (async () => { // Create a deep copy, and let's get rid of the original object let toSign:SignatureFragment | Array<SignatureFragment> = JSON.parse(fragmentData); // Make sure it's an array if (!Array.isArray(toSign)) { toSign = [toSign]; } // For all password fragments, hash the password and ditch the actual password data for (let fragment of toSign) { if (fragment.type == "password") { fragment.data = Buffer.from(await crypto.subtle.digest( 'SHA-256', new TextEncoder().encode(fragment.data) )).toString("hex"); } } // We try really hard to leave as little data at rest as possible // e.g., we don't store signed messages in memory for very long return __createSignerObject( await toSign.map<Promise<[SignatureFragment, string]>>(async (fragment) => { return [ fragment, (await provider.send('personal_sign', [ hexlify(toUtf8Bytes(fragment.data)), address ] )).replace("0x", "") ]; }).reduce(async (key, tuple) => { // Add on the unsigned data + signature for this fragment. return (await key) + (await tuple)[1] + (await tuple)[0].data; }, Promise.resolve("")) ) })(); }; export default async function freehold( provider:ethers.providers.JsonRpcProvider, address:string, toSign:SignatureFragment | Array<SignatureFragment> ) { // We call a private signer just to be extra careful. // We stringify the input becasue we don't want to hold a // a reference to the original data. return __createSigner(provider, address, JSON.stringify(toSign)); }