diff --git a/package.json b/package.json index 2311771f6e..447fa0a41b 100644 --- a/package.json +++ b/package.json @@ -138,6 +138,7 @@ "bip39": "^3.0.2", "browser-ipfs": "^0.0.6", "crypto-browserify": "^3.12.0", + "eccrypto": "^1.1.1", "ethereumjs-wallet": "^0.6.0", "events": "^3.0.0", "i18n-js": "^3.1.0", diff --git a/src/utils/backup.ts b/src/utils/backup.ts new file mode 100644 index 0000000000..f076aac9d1 --- /dev/null +++ b/src/utils/backup.ts @@ -0,0 +1,90 @@ +import { SoftwareKeyProvider } from 'jolocom-lib/js/vaultedKeyProvider/softwareProvider' +// @ts-ignore +import eccrypto from 'eccrypto' +import { ICredentialAttrs } from 'jolocom-lib/js/credentials/credential/types' +import { IKeyDerivationArgs } from 'jolocom-lib/js/vaultedKeyProvider/types' + +export interface BackupFile { + keys: EncryptedKey[] + data: string +} + +export interface EncryptedKey { + pubKey: string + cipher: string +} + +export async function createBackup( + did: string, + credentials: ICredentialAttrs[], + vault: SoftwareKeyProvider, + derivationArgs: IKeyDerivationArgs, +): Promise { + const data = { + did, + credentials, + // TODO add more data here like e.g. interaction data + } + const publicKey = vault.getPublicKey(derivationArgs) + + const symKey = SoftwareKeyProvider.getRandom(128) + // @ts-ignore private + const encryptedData: Buffer = SoftwareKeyProvider.encrypt( + symKey, + JSON.stringify(data), + ) + const encryptedKey = await eccrypto.encrypt(publicKey, Buffer.from(symKey)) + const backupFile: BackupFile = { + keys: [ + { + cipher: stringifyEncryptedData(encryptedKey), + pubKey: publicKey.toString('hex'), + }, + ], + data: encryptedData.toString('hex'), + } + return JSON.stringify(backupFile) +} + +export async function decryptBackup( + backupFile: BackupFile, + vault: SoftwareKeyProvider, + derivationArg: IKeyDerivationArgs, +): Promise { + const publicKey = vault.getPublicKey(derivationArg) + const privateKey = vault.getPrivateKey(derivationArg) + // find encrypted key + const encryptedKey = backupFile.keys.find( + key => key.pubKey === publicKey.toString('hex'), + ) + if (!encryptedKey) throw new Error('Not encrypted for these keys') + const key = await eccrypto.decrypt( + privateKey, + parseEncryptedData(encryptedKey.cipher), + ) + // @ts-ignore private + return SoftwareKeyProvider.decrypt( + key, + Buffer.from(backupFile.data, 'hex'), + ).toString() +} + +function stringifyEncryptedData(data: { + iv: Buffer + ephemPublicKey: Buffer + ciphertext: Buffer + mac: Buffer +}): string { + let hexData = {} + Object.keys(data).forEach(key => (hexData[key] = data[key].toString('hex'))) + return JSON.stringify(hexData) +} + +function parseEncryptedData(data: string): object { + const hexData = JSON.parse(data) + let bufferData = {} + Object.keys(hexData).forEach( + key => (bufferData[key] = Buffer.from(hexData[key], 'hex')), + ) + return bufferData +} diff --git a/tests/utils/backup.test.ts b/tests/utils/backup.test.ts new file mode 100644 index 0000000000..9ee356e795 --- /dev/null +++ b/tests/utils/backup.test.ts @@ -0,0 +1,42 @@ +import { createBackup, decryptBackup } from '../../src/utils/backup' +import { SoftwareKeyProvider } from 'jolocom-lib/js/vaultedKeyProvider/softwareProvider' +import { + IKeyDerivationArgs, + KeyTypes, +} from 'jolocom-lib/js/vaultedKeyProvider/types' +// @ts-ignore +import { testCreds } from '../lib/testData/filterTestData' + +describe('Backup', () => { + let vault: SoftwareKeyProvider + let derivationArgs: IKeyDerivationArgs + beforeAll(() => { + vault = SoftwareKeyProvider.recoverKeyPair( + 'unusual empty journey convince crouch castle private march dune auction middle gloom', + 'secret', + ) as SoftwareKeyProvider + derivationArgs = { + derivationPath: KeyTypes.jolocomIdentityKey, + encryptionPass: 'secret', + } + }) + + it('should encrypt and decrypt', async () => { + const data = { + did: 'did:jolo:testidstring', + credentials: testCreds.map(c => c.toJSON()), + } + const backup = await createBackup( + data.did, + data.credentials, + vault, + derivationArgs, + ) + console.log(backup) + + const recoveredData = JSON.parse( + await decryptBackup(JSON.parse(backup), vault, derivationArgs), + ) + expect(recoveredData).toEqual(data) + }) +}) diff --git a/yarn.lock b/yarn.lock index e68dd9c6e0..0089c63137 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1855,6 +1855,11 @@ acorn-walk@^6.0.1: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.1.1.tgz#d363b66f5fac5f018ff9c3a1e7b6f8e310cc3913" integrity sha512-OtUw6JUTgxA2QoqqmrmQ7F2NYqiBPi/L2jqHyFtllhOUvXYQXf0Z1CYUinIfyT4bTCGmrA7gX9FvHA81uzCoVw== +acorn@7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.0.tgz#949d36f2c292535da602283586c2477c57eb2d6c" + integrity sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ== + acorn@^5.5.3: version "5.7.3" resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279" @@ -4137,6 +4142,18 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" +eccrypto@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/eccrypto/-/eccrypto-1.1.2.tgz#7962402f923044d25a802d7369447b0a563873b1" + integrity sha512-4z/uF18h2TFdqqtFSUvlwRD9epzmeEEUZ4nVMv3ox+jy+V7AxU9s3nLoEDDbptTUlkAbAp5bPfhaS7H4naoFqg== + dependencies: + acorn "7.1.0" + elliptic "6.5.1" + es6-promise "4.2.8" + nan "2.14.0" + optionalDependencies: + secp256k1 "3.7.1" + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -4176,6 +4193,19 @@ elliptic@6.3.3: hash.js "^1.0.0" inherits "^2.0.1" +elliptic@6.5.1: + version "6.5.1" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.1.tgz#c380f5f909bf1b9b4428d028cd18d3b0efd6b52b" + integrity sha512-xvJINNLbTeWQjrl6X+7eQCrIy/YPv5XCpKW6kB5mKvtnGILoLDcySuwomfdzt0BMdLNVnuRNTuzKNHj0bva1Cg== + dependencies: + bn.js "^4.4.0" + brorand "^1.0.1" + hash.js "^1.0.0" + hmac-drbg "^1.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.0" + elliptic@^6.0.0, elliptic@^6.4.0, elliptic@^6.4.1: version "6.4.1" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.1.tgz#c2d0b7776911b86722c632c3c06c60f2f819939a" @@ -4355,7 +4385,7 @@ es6-object-assign@^1.1.0: resolved "https://registry.yarnpkg.com/es6-object-assign/-/es6-object-assign-1.1.0.tgz#c2c3582656247c39ea107cb1e6652b6f9f24523c" integrity sha1-wsNYJlYkfDnqEHyx5mUrb58kUjw= -es6-promise@^4.0.3, es6-promise@^4.0.5: +es6-promise@4.2.8, es6-promise@^4.0.3, es6-promise@^4.0.5: version "4.2.8" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== @@ -8374,7 +8404,7 @@ nan@2.13.2: resolved "https://registry.yarnpkg.com/nan/-/nan-2.13.2.tgz#f51dc7ae66ba7d5d55e1e6d4d8092e802c9aefe7" integrity sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw== -nan@^2.0.8, nan@^2.10.0, nan@^2.12.1, nan@^2.13.2, nan@^2.14.0, nan@^2.2.1, nan@^2.3.3, nan@^2.9.2: +nan@2.14.0, nan@^2.0.8, nan@^2.10.0, nan@^2.12.1, nan@^2.13.2, nan@^2.14.0, nan@^2.2.1, nan@^2.3.3, nan@^2.9.2: version "2.14.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== @@ -10792,7 +10822,7 @@ scryptsy@^1.2.1: dependencies: pbkdf2 "^3.0.3" -secp256k1@^3.0.1: +secp256k1@3.7.1, secp256k1@^3.0.1: version "3.7.1" resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-3.7.1.tgz#12e473e0e9a7c2f2d4d4818e722ad0e14cc1e2f1" integrity sha512-1cf8sbnRreXrQFdH6qsg2H71Xw91fCCS9Yp021GnUNJzWJS/py96fS4lHbnTnouLp08Xj6jBoBB6V78Tdbdu5g==