From d09a326793c45c2a58c8a30da6263fc7f3ff7668 Mon Sep 17 00:00:00 2001 From: Simonas Karuzas Date: Tue, 11 Feb 2020 15:30:31 +0200 Subject: [PATCH] fix: Using local version of DIDComm lib --- packages/daf-ethr-did-fs/package.json | 5 +- packages/daf-ethr-did-fs/src/didcomm.ts | 331 ++++++++++++++++++ .../daf-ethr-did-fs/src/identity-provider.ts | 2 +- packages/daf-ethr-did-fs/src/identity.ts | 2 +- packages/daf-ethr-did-fs/yarn-error.log | 67 +++- .../daf-ethr-did-local-storage/package.json | 5 +- .../daf-ethr-did-local-storage/src/didcomm.ts | 331 ++++++++++++++++++ .../src/identity-provider.ts | 2 +- .../src/identity.ts | 2 +- 9 files changed, 723 insertions(+), 24 deletions(-) create mode 100644 packages/daf-ethr-did-fs/src/didcomm.ts create mode 100644 packages/daf-ethr-did-local-storage/src/didcomm.ts diff --git a/packages/daf-ethr-did-fs/package.json b/packages/daf-ethr-did-fs/package.json index ec30b202e..bc0834abd 100644 --- a/packages/daf-ethr-did-fs/package.json +++ b/packages/daf-ethr-did-fs/package.json @@ -8,7 +8,7 @@ "build": "tsc" }, "dependencies": { - "DIDComm-js": "github:decentralized-identity/DIDComm-js", + "base-58": "^0.0.1", "base64url": "^3.0.1", "daf-core": "^1.4.1", "daf-resolver": "^1.1.0", @@ -17,7 +17,8 @@ "ethjs-provider-signer": "^0.1.4", "ethjs-signer": "^0.1.1", "ethr-did": "^1.1.0", - "js-sha3": "^0.8.0" + "js-sha3": "^0.8.0", + "libsodium-wrappers": "^0.7.6" }, "devDependencies": { "@types/debug": "^4.1.5", diff --git a/packages/daf-ethr-did-fs/src/didcomm.ts b/packages/daf-ethr-did-fs/src/didcomm.ts new file mode 100644 index 000000000..980767fd6 --- /dev/null +++ b/packages/daf-ethr-did-fs/src/didcomm.ts @@ -0,0 +1,331 @@ +const Base58 = require('base-58') +import sodium from 'libsodium-wrappers' + +interface IUnpackedMsg { + message: string + recipientKey: any + senderKey: string + nonRepudiableVerification: boolean +} + +interface JWSUnpacked { + content: string + verkey: string + verified: boolean +} + +export class DIDComm { + public readonly ready: Promise + private sodium: any + + /** + * Creates a new PackUnpack object. The returned object contains a .Ready property: + * a promise that must be resolved before the object can be used. You can + * simply `await` the resolution of the .Ready property. + * + * Example: + * let packUnpack = new PackUnpack + * (async () => { + * await packUnpack.Ready + * }()) + */ + constructor() { + this.ready = new Promise(async (res, rej) => { + try { + await sodium.ready + this.sodium = sodium + res() + } catch (err) { + rej(err) + } + }) + } + + /** + * Uses libsodium to generate a key pair, you may pass these keys into the pack/unpack functions + * @ + */ + public async generateKeyPair(): Promise { + return this.sodium.crypto_sign_keypair() + } + + /** + * Used to encrypt or encrypt and sign a message for one or many recipients so the recipients can authenticate the + * sender in both repudiable and non repudiable formats. By default messages should use repudiable authentication. + * This should be the most common API used. + * @param msg the message to be encrypted or encrypted and signed + * @param recipientKeys the keys which the message will be encrypted for + * @param senderKeys the keys used to encrypted or encrypt and sign a message + * @param nonRepudiable determines whether a message is encrypted only or signed and encrypted + * @returns if nonRepudiable == true returns the msg encrypted and signed as follows JWE(JWS(msg)) + * if nonRepudiable == false returns the msg encrypted as follows JWE(msg) + */ + public async pack_auth_msg_for_recipients( + msg: string, + recipientKeys: Uint8Array[], + senderKeys: sodium.KeyPair, + nonRepudiable: Boolean = false, + ): Promise { + if (nonRepudiable) { + //return JWE(JWS(msg)) + let signedMsg = await this.signContent(msg, senderKeys) + return this.packMessage(signedMsg, recipientKeys, senderKeys) + } else { + // return (JWE(msg)) + return this.packMessage(msg, recipientKeys, senderKeys) + } + } + + /** + * + * @param msg this is the message which will be anonymously encrypted for one or many recipients + * @param recipientKeys a list of the recipients keys + * @returns a JWE with an ephemeral sender key + */ + public async pack_anon_msg_for_recipients(msg: string, recipientKeys: Uint8Array[]): Promise { + return this.packMessage(msg, recipientKeys, null) + } + + /** + * + * @param msg the message to signed with non-repudiation but not encrypted + * @param senderKeys the key used to sign the + * @returns a compact JWS + */ + public async pack_nonrepudiable_msg_for_anyone(msg: string, senderKeys: sodium.KeyPair): Promise { + return this.signContent(msg, senderKeys) + } + + /** + * Unpacks a message + * @param encMsg message to be decrypted + * @param toKeys key pair of party decrypting the message + */ + public async unpackMessage(packedMsg: string, toKeys: sodium.KeyPair): Promise { + try { + return await this.unpackEncrypted(packedMsg, toKeys) + } catch (err) { + let jwsChecked = this.verifyContent(packedMsg) + return { + message: jwsChecked.content, + recipientKey: null, + senderKey: jwsChecked.verkey, + nonRepudiableVerification: jwsChecked.verified, + } + } + } + + /** + * + * Packs a message. + * @param message string message to be encrypted + * @param toKeys public key of the entity encrypting message for + * @param fromKeys keypair of person encrypting message + */ + private async packMessage( + msg: string, + recipientKeys: Uint8Array[], + fromKeys: sodium.KeyPair | null = null, + ): Promise { + let [recipsJson, cek] = this.prepareRecipientKeys(recipientKeys, fromKeys) + let recipsB64 = this.b64url(recipsJson) + + let [ciphertext, tag, iv] = this.encryptPlaintext(msg, recipsB64, cek) + + return JSON.stringify({ + ciphertext: this.b64url(ciphertext), + iv: this.b64url(iv), + protected: recipsB64, + tag: this.b64url(tag), + }) + } + + private async unpackEncrypted(encMsg: string, toKeys: sodium.KeyPair): Promise { + let wrapper + if (typeof encMsg === 'string') { + wrapper = JSON.parse(encMsg) + } else { + wrapper = encMsg + } + if (typeof toKeys.publicKey === 'string') { + toKeys.publicKey = Base58.decode(toKeys.publicKey) + } + if (typeof toKeys.privateKey === 'string') { + toKeys.privateKey = Base58.decode(toKeys.privateKey) + } + let recipsJson = this.strB64dec(wrapper.protected) + let recipsOuter = JSON.parse(recipsJson) + + let alg = recipsOuter.alg + let isAuthcrypt = alg === 'Authcrypt' + if (!isAuthcrypt && alg !== 'Anoncrypt') { + throw new Error('Unsupported pack algorithm: ' + alg) + } + let [cek, senderVk, recipVk] = this.locateRecKey(recipsOuter.recipients, toKeys) + if (!senderVk && isAuthcrypt) { + throw new Error('Sender public key not provided in Authcrypt message') + } + let ciphertext = this.b64dec(wrapper.ciphertext) + let nonce = this.b64dec(wrapper.iv) + let tag = this.b64dec(wrapper.tag) + + let message = this.decryptPlaintext(ciphertext, tag, wrapper.protected, nonce, cek) + try { + let jwsVerified = this.verifyContent(message) + return { + message: jwsVerified.content, + recipientKey: recipVk, + senderKey: senderVk, + nonRepudiableVerification: senderVk === jwsVerified.verkey ? true : false, + } + } catch (err) { + return { + message, + recipientKey: recipVk, + senderKey: senderVk, + nonRepudiableVerification: false, + } + } + } + + private async signContent(msg: string, signerKeyPair: sodium.KeyPair): Promise { + // get public key base58 encoded + let senderVk = Base58.encode(signerKeyPair.publicKey) + + // generate jose header, b64url encode it, and concat to b64url encoded payload + let joseHeader = { + alg: 'EdDSA', + kid: senderVk, + } + let joseString = JSON.stringify(joseHeader) + let b64JoseStr = this.b64url(joseString) + let b64Payload = this.b64url(msg) + let headerAndPayloadConcat = `${b64JoseStr}.${b64Payload}` + + //sign data and return compact JWS + let signature = this.b64url(sodium.crypto_sign(headerAndPayloadConcat, signerKeyPair.privateKey)) + return `${headerAndPayloadConcat}.${signature}` + } + + private verifyContent(jws: string): JWSUnpacked { + let jwsSplit = jws.split('.') + let joseHeader = JSON.parse(this.strB64dec(jwsSplit[0])) + if (joseHeader.alg != 'EdDSA') { + throw 'Cryptographic algorithm unidentifiable' + } + let sigMsg = sodium.crypto_sign_open(this.b64dec(jwsSplit[2]), Base58.decode(joseHeader.kid)) + + return { + content: this.strB64dec(jwsSplit[1]), + verkey: joseHeader.kid, + verified: sodium.to_string(sigMsg) === `${jwsSplit[0]}.${jwsSplit[1]}` ? true : false, + } + } + + b64url(input: any) { + return this.sodium.to_base64(input, this.sodium.base64_variants.URLSAFE) + } + + b64dec(input: any) { + return this.sodium.from_base64(input, this.sodium.base64_variants.URLSAFE) + } + + private strB64dec(input: any) { + return this.sodium.to_string(this.sodium.from_base64(input, this.sodium.base64_variants.URLSAFE)) + } + + private encryptPlaintext(message: any, addData: any, key: any) { + let iv = this.sodium.randombytes_buf(this.sodium.crypto_aead_chacha20poly1305_ietf_NPUBBYTES) + let out = this.sodium.crypto_aead_chacha20poly1305_ietf_encrypt_detached(message, addData, null, iv, key) + return [out.ciphertext, out.mac, iv] + } + + private decryptPlaintext(ciphertext: any, mac: any, recipsBin: any, nonce: any, key: any) { + return this.sodium.to_string( + this.sodium.crypto_aead_chacha20poly1305_ietf_decrypt_detached( + null, // nsec + ciphertext, + mac, + recipsBin, // ad + nonce, // npub + key, + ), + ) + } + + private prepareRecipientKeys(toKeys: any, fromKeys: any = null) { + let cek = this.sodium.crypto_aead_chacha20poly1305_ietf_keygen() + let recips: any[] = [] + + toKeys.forEach((targetVk: any) => { + let encCek = null + let encSender = null + let nonce = null + + let targetPk = this.sodium.crypto_sign_ed25519_pk_to_curve25519(targetVk) + + if (fromKeys) { + let senderVk = Base58.encode(fromKeys.publicKey) + let senderSk = this.sodium.crypto_sign_ed25519_sk_to_curve25519(fromKeys.privateKey) + encSender = this.sodium.crypto_box_seal(senderVk, targetPk) + + nonce = this.sodium.randombytes_buf(this.sodium.crypto_box_NONCEBYTES) + encCek = this.sodium.crypto_box_easy(cek, nonce, targetPk, senderSk) + } else { + encCek = this.sodium.crypto_box_seal(cek, targetPk) + } + + recips.push({ + encrypted_key: this.b64url(encCek), + header: { + iv: nonce ? this.b64url(nonce) : null, + kid: Base58.encode(targetVk), + sender: encSender ? this.b64url(encSender) : null, + }, + }) + }) + + let data = { + alg: fromKeys ? 'Authcrypt' : 'Anoncrypt', + enc: 'chacha20poly1305_ietf', + recipients: recips, + typ: 'JWM/1.0', + } + return [JSON.stringify(data), cek] + } + + private locateRecKey(recipients: any, keys: any) { + let notFound = [] + /* tslint:disable */ + for (let index in recipients) { + let recip = recipients[index] + if (!('header' in recip) || !('encrypted_key' in recip)) { + throw new Error('Invalid recipient header') + } + + let recipVk = Base58.decode(recip.header.kid) + if (!this.sodium.memcmp(recipVk, keys.publicKey)) { + notFound.push(recip.header.kid) + } + let pk = this.sodium.crypto_sign_ed25519_pk_to_curve25519(keys.publicKey) + let sk = this.sodium.crypto_sign_ed25519_sk_to_curve25519(keys.privateKey) + + let encrytpedKey = this.b64dec(recip.encrypted_key) + let nonce = recip.header.iv ? this.b64dec(recip.header.iv) : null + let encSender = recip.header.sender ? this.b64dec(recip.header.sender) : null + + let senderVk = null + let cek = null + if (nonce && encSender) { + senderVk = this.sodium.to_string(this.sodium.crypto_box_seal_open(encSender, pk, sk)) + let senderPk = this.sodium.crypto_sign_ed25519_pk_to_curve25519(Base58.decode(senderVk)) + cek = this.sodium.crypto_box_open_easy(encrytpedKey, nonce, senderPk, sk) + } else { + cek = this.sodium.crypto_box_seal_open(encrytpedKey, pk, sk) + } + return [cek, senderVk, recip.header.kid] + } + + throw new Error('No corresponding recipient key found in recipients') + } +} diff --git a/packages/daf-ethr-did-fs/src/identity-provider.ts b/packages/daf-ethr-did-fs/src/identity-provider.ts index 8a49e6fc4..6c24fff36 100644 --- a/packages/daf-ethr-did-fs/src/identity-provider.ts +++ b/packages/daf-ethr-did-fs/src/identity-provider.ts @@ -8,7 +8,7 @@ import Debug from 'debug' const debug = Debug('daf:ethr-did-fs:identity-provider') const EC = require('elliptic').ec const secp256k1 = new EC('secp256k1') -import { DIDComm } from 'DIDComm-js' +import { DIDComm } from './didcomm' const didcomm = new DIDComm() import { keccak_256 } from 'js-sha3' diff --git a/packages/daf-ethr-did-fs/src/identity.ts b/packages/daf-ethr-did-fs/src/identity.ts index b45049722..61f693728 100644 --- a/packages/daf-ethr-did-fs/src/identity.ts +++ b/packages/daf-ethr-did-fs/src/identity.ts @@ -1,7 +1,7 @@ import { AbstractIdentity, Resolver } from 'daf-core' import { SimpleSigner } from 'did-jwt' import { Key } from './identity-provider' -import { DIDComm } from 'DIDComm-js' +import { DIDComm } from './didcomm' const didcomm = new DIDComm() export class Identity extends AbstractIdentity { diff --git a/packages/daf-ethr-did-fs/yarn-error.log b/packages/daf-ethr-did-fs/yarn-error.log index 12409d297..6a5125c0e 100644 --- a/packages/daf-ethr-did-fs/yarn-error.log +++ b/packages/daf-ethr-did-fs/yarn-error.log @@ -1,11 +1,11 @@ Arguments: - /Users/simonas/.nvm/versions/node/v12.13.1/bin/node /Users/simonas/.nvm/versions/node/v12.13.1/bin/yarn add @types/ethjs-provider-signer -D + /Users/simonas/.nvm/versions/node/v12.13.1/bin/node /Users/simonas/.yarn/bin/yarn.js add @types/base-58 PATH: - /Users/simonas/.nvm/versions/node/v12.13.1/bin:/Users/simonas/.cargo/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Users/simonas/.nvm/versions/node/v12.13.1/bin:/Users/simonas/.cargo/bin:/Users/simonas/Library/Android/sdk/tools:/Users/simonas/Library/Android/sdk/platform-tools:/Users/simonas/Library/Android/sdk/tools:/Users/simonas/Library/Android/sdk/platform-tools + /Users/simonas/.yarn/bin:/Users/simonas/.config/yarn/global/node_modules/.bin:/Users/simonas/.nvm/versions/node/v12.13.1/bin:/Users/simonas/.cargo/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Users/simonas/.yarn/bin:/Users/simonas/.config/yarn/global/node_modules/.bin:/Users/simonas/.nvm/versions/node/v12.13.1/bin:/Users/simonas/.cargo/bin:/Users/simonas/Library/Android/sdk/tools:/Users/simonas/Library/Android/sdk/platform-tools:/Users/simonas/Library/Android/sdk/tools:/Users/simonas/Library/Android/sdk/platform-tools Yarn version: - 1.19.1 + 1.21.1 Node version: 12.13.1 @@ -14,13 +14,13 @@ Platform: darwin x64 Trace: - Error: https://registry.yarnpkg.com/@types%2fethjs-provider-signer: Not found - at Request.params.callback [as _callback] (/Users/simonas/.nvm/versions/node/v12.13.1/lib/node_modules/yarn/lib/cli.js:66926:18) - at Request.self.callback (/Users/simonas/.nvm/versions/node/v12.13.1/lib/node_modules/yarn/lib/cli.js:140564:22) + Error: https://registry.yarnpkg.com/@types%2fbase-58: Not found + at Request.params.callback [as _callback] (/Users/simonas/.yarn/lib/cli.js:66947:18) + at Request.self.callback (/Users/simonas/.yarn/lib/cli.js:140665:22) at Request.emit (events.js:210:5) - at Request. (/Users/simonas/.nvm/versions/node/v12.13.1/lib/node_modules/yarn/lib/cli.js:141536:10) + at Request. (/Users/simonas/.yarn/lib/cli.js:141637:10) at Request.emit (events.js:210:5) - at IncomingMessage. (/Users/simonas/.nvm/versions/node/v12.13.1/lib/node_modules/yarn/lib/cli.js:141458:12) + at IncomingMessage. (/Users/simonas/.yarn/lib/cli.js:141559:12) at Object.onceWrapper (events.js:299:28) at IncomingMessage.emit (events.js:215:7) at endReadableNT (_stream_readable.js:1184:12) @@ -37,17 +37,21 @@ npm manifest: "build": "tsc" }, "dependencies": { + "DIDComm-js": "github:decentralized-identity/DIDComm-js", + "base-58": "^0.0.1", + "base64url": "^3.0.1", "daf-core": "^1.4.1", + "daf-resolver": "^1.1.0", "debug": "^4.1.1", + "elliptic": "^6.5.2", "ethjs-provider-signer": "^0.1.4", "ethjs-signer": "^0.1.1", - "ethr-did": "^1.1.0" + "ethr-did": "^1.1.0", + "js-sha3": "^0.8.0" }, "devDependencies": { "@types/debug": "^4.1.5", "@types/ethjs-signer": "^0.1.0", - "ethjs-provider-signer": "^0.1.4", - "ethjs-signer": "^0.1.1", "typescript": "^3.7.2" }, "files": [ @@ -2779,7 +2783,7 @@ Lockfile: "@types/koa-compose" "*" "@types/node" "*" - "@types/libsodium-wrappers@^0.7.5", "@types/libsodium-wrappers@^0.7.7": + "@types/libsodium-wrappers@^0.7.5": version "0.7.7" resolved "https://registry.yarnpkg.com/@types/libsodium-wrappers/-/libsodium-wrappers-0.7.7.tgz#cdb25e85458612ec80f0157c3815fac187d0b6d2" integrity sha512-Li91pVKcLvQJK3ZolwCPo85oxf2gKBCApgnesRxYg4OVYchLXcJB2eivX8S87vfQVv6ZRnyCO1lLDosZGJfpRg== @@ -3199,6 +3203,14 @@ Lockfile: base-58 "0.0.1" libsodium-wrappers "^0.7.5" + "DIDComm-js@github:decentralized-identity/DIDComm-js": + version "0.6.0" + resolved "https://codeload.github.com/decentralized-identity/DIDComm-js/tar.gz/c8ca38df0b78c03d4e3c7e4cde13bd0a933e284a" + dependencies: + "@types/libsodium-wrappers" "^0.7.5" + base-58 "0.0.1" + libsodium-wrappers "^0.7.5" + JSONStream@^1.0.4, JSONStream@^1.3.4, JSONStream@^1.3.5: version "1.3.5" resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" @@ -4133,7 +4145,7 @@ Lockfile: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= - base-58@0.0.1: + base-58@0.0.1, base-58@^0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/base-58/-/base-58-0.0.1.tgz#85d3e70251075661933388f831d1eb8b8f6314e3" integrity sha1-hdPnAlEHVmGTM4j4MdHri49jFOM= @@ -5800,9 +5812,16 @@ Lockfile: daf-ethr-did-local-storage@../daf/packages/daf-ethr-did-local-storage: version "1.4.1" dependencies: + "@types/ethjs-signer" "^0.1.0" + DIDComm-js "github:decentralized-identity/DIDComm-js" + base64url "^3.0.1" daf-core "^1.4.1" debug "^4.1.1" + elliptic "^6.5.2" + ethjs-provider-signer "^0.1.4" + ethjs-signer "^0.1.1" ethr-did "^1.1.0" + js-sha3 "^0.8.0" daf-ethr-did-metamask@../daf/packages/daf-ethr-did-metamask: version "1.4.1" @@ -6379,7 +6398,7 @@ Lockfile: hash.js "^1.0.0" inherits "^2.0.1" - elliptic@^6.0.0, elliptic@^6.4.0: + elliptic@^6.0.0, elliptic@^6.4.0, elliptic@^6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.2.tgz#05c5678d7173c049d8ca433552224a495d0e3762" integrity sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw== @@ -10118,7 +10137,7 @@ Lockfile: y18n "^4.0.0" yargs "^11.0.0" - libsodium-wrappers@^0.7.5, libsodium-wrappers@^0.7.6: + libsodium-wrappers@^0.7.5: version "0.7.6" resolved "https://registry.yarnpkg.com/libsodium-wrappers/-/libsodium-wrappers-0.7.6.tgz#baed4c16d4bf9610104875ad8a8e164d259d48fb" integrity sha512-OUO2CWW5bHdLr6hkKLHIKI4raEkZrf3QHkhXsJ1yCh6MZ3JDA7jFD3kCATNquuGSG6MjjPHQIQms0y0gBDzjQg== @@ -15681,6 +15700,17 @@ Lockfile: source-map-support "^0.5.6" yn "^3.0.0" + ts-node@^8.6.2: + version "8.6.2" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.6.2.tgz#7419a01391a818fbafa6f826a33c1a13e9464e35" + integrity sha512-4mZEbofxGqLL2RImpe3zMJukvEvcO1XP8bj8ozBPySdCUXEcU5cIRwR0aM3R+VoZq7iXc8N86NC0FspGRqP4gg== + dependencies: + arg "^4.1.0" + diff "^4.0.1" + make-error "^1.1.1" + source-map-support "^0.5.6" + yn "3.1.1" + ts-pnp@1.1.5, ts-pnp@^1.1.2: version "1.1.5" resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.1.5.tgz#840e0739c89fce5f3abd9037bb091dbff16d9dec" @@ -15775,6 +15805,11 @@ Lockfile: resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.3.tgz#b36840668a16458a7025b9eabfad11b66ab85c69" integrity sha512-Mcr/Qk7hXqFBXMN7p7Lusj1ktCBydylfQM/FZCk5glCNQJrCUKPkMHdo9R0MTFWsC/4kPFvDS0fDPvukfCkFsw== + typescript@^3.7.5: + version "3.7.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.5.tgz#0692e21f65fd4108b9330238aac11dd2e177a1ae" + integrity sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw== + ua-parser-js@^0.7.18: version "0.7.20" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.20.tgz#7527178b82f6a62a0f243d1f94fd30e3e3c21098" @@ -16857,7 +16892,7 @@ Lockfile: resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419" integrity sha1-AI4G2AlDIMNy28L47XagymyKxBk= - yn@^3.0.0: + yn@3.1.1, yn@^3.0.0: version "3.1.1" resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== diff --git a/packages/daf-ethr-did-local-storage/package.json b/packages/daf-ethr-did-local-storage/package.json index 424ec615d..0153bcfaf 100644 --- a/packages/daf-ethr-did-local-storage/package.json +++ b/packages/daf-ethr-did-local-storage/package.json @@ -9,7 +9,7 @@ }, "dependencies": { "@types/ethjs-signer": "^0.1.0", - "DIDComm-js": "github:decentralized-identity/DIDComm-js", + "base-58": "^0.0.1", "base64url": "^3.0.1", "daf-core": "^1.4.1", "debug": "^4.1.1", @@ -17,7 +17,8 @@ "ethjs-provider-signer": "^0.1.4", "ethjs-signer": "^0.1.1", "ethr-did": "^1.1.0", - "js-sha3": "^0.8.0" + "js-sha3": "^0.8.0", + "libsodium-wrappers": "^0.7.6" }, "devDependencies": { "@types/debug": "^4.1.5", diff --git a/packages/daf-ethr-did-local-storage/src/didcomm.ts b/packages/daf-ethr-did-local-storage/src/didcomm.ts new file mode 100644 index 000000000..980767fd6 --- /dev/null +++ b/packages/daf-ethr-did-local-storage/src/didcomm.ts @@ -0,0 +1,331 @@ +const Base58 = require('base-58') +import sodium from 'libsodium-wrappers' + +interface IUnpackedMsg { + message: string + recipientKey: any + senderKey: string + nonRepudiableVerification: boolean +} + +interface JWSUnpacked { + content: string + verkey: string + verified: boolean +} + +export class DIDComm { + public readonly ready: Promise + private sodium: any + + /** + * Creates a new PackUnpack object. The returned object contains a .Ready property: + * a promise that must be resolved before the object can be used. You can + * simply `await` the resolution of the .Ready property. + * + * Example: + * let packUnpack = new PackUnpack + * (async () => { + * await packUnpack.Ready + * }()) + */ + constructor() { + this.ready = new Promise(async (res, rej) => { + try { + await sodium.ready + this.sodium = sodium + res() + } catch (err) { + rej(err) + } + }) + } + + /** + * Uses libsodium to generate a key pair, you may pass these keys into the pack/unpack functions + * @ + */ + public async generateKeyPair(): Promise { + return this.sodium.crypto_sign_keypair() + } + + /** + * Used to encrypt or encrypt and sign a message for one or many recipients so the recipients can authenticate the + * sender in both repudiable and non repudiable formats. By default messages should use repudiable authentication. + * This should be the most common API used. + * @param msg the message to be encrypted or encrypted and signed + * @param recipientKeys the keys which the message will be encrypted for + * @param senderKeys the keys used to encrypted or encrypt and sign a message + * @param nonRepudiable determines whether a message is encrypted only or signed and encrypted + * @returns if nonRepudiable == true returns the msg encrypted and signed as follows JWE(JWS(msg)) + * if nonRepudiable == false returns the msg encrypted as follows JWE(msg) + */ + public async pack_auth_msg_for_recipients( + msg: string, + recipientKeys: Uint8Array[], + senderKeys: sodium.KeyPair, + nonRepudiable: Boolean = false, + ): Promise { + if (nonRepudiable) { + //return JWE(JWS(msg)) + let signedMsg = await this.signContent(msg, senderKeys) + return this.packMessage(signedMsg, recipientKeys, senderKeys) + } else { + // return (JWE(msg)) + return this.packMessage(msg, recipientKeys, senderKeys) + } + } + + /** + * + * @param msg this is the message which will be anonymously encrypted for one or many recipients + * @param recipientKeys a list of the recipients keys + * @returns a JWE with an ephemeral sender key + */ + public async pack_anon_msg_for_recipients(msg: string, recipientKeys: Uint8Array[]): Promise { + return this.packMessage(msg, recipientKeys, null) + } + + /** + * + * @param msg the message to signed with non-repudiation but not encrypted + * @param senderKeys the key used to sign the + * @returns a compact JWS + */ + public async pack_nonrepudiable_msg_for_anyone(msg: string, senderKeys: sodium.KeyPair): Promise { + return this.signContent(msg, senderKeys) + } + + /** + * Unpacks a message + * @param encMsg message to be decrypted + * @param toKeys key pair of party decrypting the message + */ + public async unpackMessage(packedMsg: string, toKeys: sodium.KeyPair): Promise { + try { + return await this.unpackEncrypted(packedMsg, toKeys) + } catch (err) { + let jwsChecked = this.verifyContent(packedMsg) + return { + message: jwsChecked.content, + recipientKey: null, + senderKey: jwsChecked.verkey, + nonRepudiableVerification: jwsChecked.verified, + } + } + } + + /** + * + * Packs a message. + * @param message string message to be encrypted + * @param toKeys public key of the entity encrypting message for + * @param fromKeys keypair of person encrypting message + */ + private async packMessage( + msg: string, + recipientKeys: Uint8Array[], + fromKeys: sodium.KeyPair | null = null, + ): Promise { + let [recipsJson, cek] = this.prepareRecipientKeys(recipientKeys, fromKeys) + let recipsB64 = this.b64url(recipsJson) + + let [ciphertext, tag, iv] = this.encryptPlaintext(msg, recipsB64, cek) + + return JSON.stringify({ + ciphertext: this.b64url(ciphertext), + iv: this.b64url(iv), + protected: recipsB64, + tag: this.b64url(tag), + }) + } + + private async unpackEncrypted(encMsg: string, toKeys: sodium.KeyPair): Promise { + let wrapper + if (typeof encMsg === 'string') { + wrapper = JSON.parse(encMsg) + } else { + wrapper = encMsg + } + if (typeof toKeys.publicKey === 'string') { + toKeys.publicKey = Base58.decode(toKeys.publicKey) + } + if (typeof toKeys.privateKey === 'string') { + toKeys.privateKey = Base58.decode(toKeys.privateKey) + } + let recipsJson = this.strB64dec(wrapper.protected) + let recipsOuter = JSON.parse(recipsJson) + + let alg = recipsOuter.alg + let isAuthcrypt = alg === 'Authcrypt' + if (!isAuthcrypt && alg !== 'Anoncrypt') { + throw new Error('Unsupported pack algorithm: ' + alg) + } + let [cek, senderVk, recipVk] = this.locateRecKey(recipsOuter.recipients, toKeys) + if (!senderVk && isAuthcrypt) { + throw new Error('Sender public key not provided in Authcrypt message') + } + let ciphertext = this.b64dec(wrapper.ciphertext) + let nonce = this.b64dec(wrapper.iv) + let tag = this.b64dec(wrapper.tag) + + let message = this.decryptPlaintext(ciphertext, tag, wrapper.protected, nonce, cek) + try { + let jwsVerified = this.verifyContent(message) + return { + message: jwsVerified.content, + recipientKey: recipVk, + senderKey: senderVk, + nonRepudiableVerification: senderVk === jwsVerified.verkey ? true : false, + } + } catch (err) { + return { + message, + recipientKey: recipVk, + senderKey: senderVk, + nonRepudiableVerification: false, + } + } + } + + private async signContent(msg: string, signerKeyPair: sodium.KeyPair): Promise { + // get public key base58 encoded + let senderVk = Base58.encode(signerKeyPair.publicKey) + + // generate jose header, b64url encode it, and concat to b64url encoded payload + let joseHeader = { + alg: 'EdDSA', + kid: senderVk, + } + let joseString = JSON.stringify(joseHeader) + let b64JoseStr = this.b64url(joseString) + let b64Payload = this.b64url(msg) + let headerAndPayloadConcat = `${b64JoseStr}.${b64Payload}` + + //sign data and return compact JWS + let signature = this.b64url(sodium.crypto_sign(headerAndPayloadConcat, signerKeyPair.privateKey)) + return `${headerAndPayloadConcat}.${signature}` + } + + private verifyContent(jws: string): JWSUnpacked { + let jwsSplit = jws.split('.') + let joseHeader = JSON.parse(this.strB64dec(jwsSplit[0])) + if (joseHeader.alg != 'EdDSA') { + throw 'Cryptographic algorithm unidentifiable' + } + let sigMsg = sodium.crypto_sign_open(this.b64dec(jwsSplit[2]), Base58.decode(joseHeader.kid)) + + return { + content: this.strB64dec(jwsSplit[1]), + verkey: joseHeader.kid, + verified: sodium.to_string(sigMsg) === `${jwsSplit[0]}.${jwsSplit[1]}` ? true : false, + } + } + + b64url(input: any) { + return this.sodium.to_base64(input, this.sodium.base64_variants.URLSAFE) + } + + b64dec(input: any) { + return this.sodium.from_base64(input, this.sodium.base64_variants.URLSAFE) + } + + private strB64dec(input: any) { + return this.sodium.to_string(this.sodium.from_base64(input, this.sodium.base64_variants.URLSAFE)) + } + + private encryptPlaintext(message: any, addData: any, key: any) { + let iv = this.sodium.randombytes_buf(this.sodium.crypto_aead_chacha20poly1305_ietf_NPUBBYTES) + let out = this.sodium.crypto_aead_chacha20poly1305_ietf_encrypt_detached(message, addData, null, iv, key) + return [out.ciphertext, out.mac, iv] + } + + private decryptPlaintext(ciphertext: any, mac: any, recipsBin: any, nonce: any, key: any) { + return this.sodium.to_string( + this.sodium.crypto_aead_chacha20poly1305_ietf_decrypt_detached( + null, // nsec + ciphertext, + mac, + recipsBin, // ad + nonce, // npub + key, + ), + ) + } + + private prepareRecipientKeys(toKeys: any, fromKeys: any = null) { + let cek = this.sodium.crypto_aead_chacha20poly1305_ietf_keygen() + let recips: any[] = [] + + toKeys.forEach((targetVk: any) => { + let encCek = null + let encSender = null + let nonce = null + + let targetPk = this.sodium.crypto_sign_ed25519_pk_to_curve25519(targetVk) + + if (fromKeys) { + let senderVk = Base58.encode(fromKeys.publicKey) + let senderSk = this.sodium.crypto_sign_ed25519_sk_to_curve25519(fromKeys.privateKey) + encSender = this.sodium.crypto_box_seal(senderVk, targetPk) + + nonce = this.sodium.randombytes_buf(this.sodium.crypto_box_NONCEBYTES) + encCek = this.sodium.crypto_box_easy(cek, nonce, targetPk, senderSk) + } else { + encCek = this.sodium.crypto_box_seal(cek, targetPk) + } + + recips.push({ + encrypted_key: this.b64url(encCek), + header: { + iv: nonce ? this.b64url(nonce) : null, + kid: Base58.encode(targetVk), + sender: encSender ? this.b64url(encSender) : null, + }, + }) + }) + + let data = { + alg: fromKeys ? 'Authcrypt' : 'Anoncrypt', + enc: 'chacha20poly1305_ietf', + recipients: recips, + typ: 'JWM/1.0', + } + return [JSON.stringify(data), cek] + } + + private locateRecKey(recipients: any, keys: any) { + let notFound = [] + /* tslint:disable */ + for (let index in recipients) { + let recip = recipients[index] + if (!('header' in recip) || !('encrypted_key' in recip)) { + throw new Error('Invalid recipient header') + } + + let recipVk = Base58.decode(recip.header.kid) + if (!this.sodium.memcmp(recipVk, keys.publicKey)) { + notFound.push(recip.header.kid) + } + let pk = this.sodium.crypto_sign_ed25519_pk_to_curve25519(keys.publicKey) + let sk = this.sodium.crypto_sign_ed25519_sk_to_curve25519(keys.privateKey) + + let encrytpedKey = this.b64dec(recip.encrypted_key) + let nonce = recip.header.iv ? this.b64dec(recip.header.iv) : null + let encSender = recip.header.sender ? this.b64dec(recip.header.sender) : null + + let senderVk = null + let cek = null + if (nonce && encSender) { + senderVk = this.sodium.to_string(this.sodium.crypto_box_seal_open(encSender, pk, sk)) + let senderPk = this.sodium.crypto_sign_ed25519_pk_to_curve25519(Base58.decode(senderVk)) + cek = this.sodium.crypto_box_open_easy(encrytpedKey, nonce, senderPk, sk) + } else { + cek = this.sodium.crypto_box_seal_open(encrytpedKey, pk, sk) + } + return [cek, senderVk, recip.header.kid] + } + + throw new Error('No corresponding recipient key found in recipients') + } +} diff --git a/packages/daf-ethr-did-local-storage/src/identity-provider.ts b/packages/daf-ethr-did-local-storage/src/identity-provider.ts index fd180ed13..f44da3fd4 100644 --- a/packages/daf-ethr-did-local-storage/src/identity-provider.ts +++ b/packages/daf-ethr-did-local-storage/src/identity-provider.ts @@ -7,7 +7,7 @@ import Debug from 'debug' const debug = Debug('daf:ethr-did-local-storage:identity-provider') const EC = require('elliptic').ec const secp256k1 = new EC('secp256k1') -import { DIDComm } from 'DIDComm-js' +import { DIDComm } from './didcomm' const didcomm = new DIDComm() import { keccak_256 } from 'js-sha3' diff --git a/packages/daf-ethr-did-local-storage/src/identity.ts b/packages/daf-ethr-did-local-storage/src/identity.ts index b45049722..61f693728 100644 --- a/packages/daf-ethr-did-local-storage/src/identity.ts +++ b/packages/daf-ethr-did-local-storage/src/identity.ts @@ -1,7 +1,7 @@ import { AbstractIdentity, Resolver } from 'daf-core' import { SimpleSigner } from 'did-jwt' import { Key } from './identity-provider' -import { DIDComm } from 'DIDComm-js' +import { DIDComm } from './didcomm' const didcomm = new DIDComm() export class Identity extends AbstractIdentity {