From 20651e5e8968686b12bdb70730e5c0db3a7da4d0 Mon Sep 17 00:00:00 2001 From: Rudy-Perrin Date: Sat, 23 Mar 2024 20:27:28 +0100 Subject: [PATCH 01/10] feat(crypto): getGenesisAddress Generates the genesis address from a given seed --- src/crypto.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/crypto.ts b/src/crypto.ts index 50759ad6..2c2efd74 100644 --- a/src/crypto.ts +++ b/src/crypto.ts @@ -584,3 +584,12 @@ function aesAuthDecrypt(encrypted: Uint8Array, aesKey: Uint8Array, iv: Uint8Arra } return hexToUint8Array(sjcl.codec.hex.fromBits(decrypted)); } + +/** + * Generates the genesis address (the first address) from a given seed + * @param {string | Uint8Array} seed The seed used to generate the address. Can be a string or a Uint8Array + * @returns {string} The genesis address in hexadecimal format + */ +export function getGenesisAddress(seed: string | Uint8Array) { + return uint8ArrayToHex(deriveAddress(seed, 0)); +} From 5eb10c643840cda0a324456c0a430a71d06d58c3 Mon Sep 17 00:00:00 2001 From: Rudy-Perrin Date: Sat, 23 Mar 2024 19:05:30 +0100 Subject: [PATCH 02/10] feat(crypto): getServiceGenesisAddress Derives the genesis address for a given service from a keychain --- src/crypto.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/crypto.ts b/src/crypto.ts index 2c2efd74..2040f8ae 100644 --- a/src/crypto.ts +++ b/src/crypto.ts @@ -22,6 +22,7 @@ import sha3 from "js-sha3"; import ed2curve from "ed2curve"; // @ts-ignore import sjcl from "sjcl"; +import Keychain from "./keychain.js"; const { sha3_512, sha3_256 } = sha3; const EC = ec; @@ -593,3 +594,14 @@ function aesAuthDecrypt(encrypted: Uint8Array, aesKey: Uint8Array, iv: Uint8Arra export function getGenesisAddress(seed: string | Uint8Array) { return uint8ArrayToHex(deriveAddress(seed, 0)); } + +/** + * Derives the genesis address for a given service from a keychain + * @param {Keychain} keychain The keychain used to derive the address + * @param {string} service The service for which to derive the address + * @param {string} [suffix=""] An optional suffix to append to the service before deriving the address + * @returns {string} The genesis address for the service in hexadecimal format + */ +export function getServiceGenesisAddress(keychain: Keychain, service: string, suffix = "") { + return uint8ArrayToHex(keychain.deriveAddress(service, 0, suffix)); +} From acc6dba5723c347352cbed395fb5edce2bae931e Mon Sep 17 00:00:00 2001 From: Rudy-Perrin Date: Wed, 3 Apr 2024 16:28:53 +0200 Subject: [PATCH 03/10] feat(crypto): encryptSecret Encrypts a secret using a given public key --- src/crypto.ts | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/src/crypto.ts b/src/crypto.ts index 2040f8ae..1d873000 100644 --- a/src/crypto.ts +++ b/src/crypto.ts @@ -1,10 +1,11 @@ -import { Curve, HashAlgorithm, Keypair } from "./types.js"; +import { AuthorizedKeyUserInput, Curve, HashAlgorithm, Keypair } from "./types.js"; import { concatUint8Arrays, hexToUint8Array, intToUint8Array, maybeHexToUint8Array, maybeStringToUint8Array, + maybeUint8ArrayToHex, uint8ArrayToHex, wordArrayToUint8Array } from "./utils.js"; @@ -605,3 +606,33 @@ export function getGenesisAddress(seed: string | Uint8Array) { export function getServiceGenesisAddress(keychain: Keychain, service: string, suffix = "") { return uint8ArrayToHex(keychain.deriveAddress(service, 0, suffix)); } +/** + * Encrypts a secret using a given public key + * @param {string | Uint8Array} secret The secret to encrypt + * @param {string | Uint8Array} publicKey The public key to use for encryption + * @returns {Object} An object containing the encrypted secret and an array of authorized keys, each with an encrypted secret key and a public key + * @example + * const storageNoncePublicKey = await archethic.network.getStorageNoncePublicKey(); + * const { encryptedSecret, authorizedKeys } = encryptSecret(Crypto.randomSecretKey(), storageNoncePublicKey); + * const code = "" // The contract code + * const tx = await archethic.transaction + * .new() + * .setType("contract") + * .setCode(code) + * .addOwnership(encryptedSecret, authorizedKeys) + * .build(seed, 0) + * .originSign(originPrivateKey) + * .send(); + */ +export function encryptSecret( + secret: string | Uint8Array, + publicKey: string | Uint8Array +): { encryptedSecret: Uint8Array; authorizedKeys: AuthorizedKeyUserInput[] } { + const aesKey = randomSecretKey(); + const encryptedSecret = aesEncrypt(secret, aesKey); + const encryptedAesKey = uint8ArrayToHex(ecEncrypt(aesKey, publicKey)); + const authorizedKeys: AuthorizedKeyUserInput[] = [ + { encryptedSecretKey: encryptedAesKey, publicKey: maybeUint8ArrayToHex(publicKey) } + ]; + return { encryptedSecret, authorizedKeys }; +} From d9fca76512c2865ce5c392ff1cd18e62ff4b5b58 Mon Sep 17 00:00:00 2001 From: Rudy-Perrin Date: Fri, 12 Apr 2024 02:37:43 +0200 Subject: [PATCH 04/10] test(crypto): add test for encryptSecret --- tests/crypto.test.ts | 68 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/tests/crypto.test.ts b/tests/crypto.test.ts index fc5cda21..b64003bd 100644 --- a/tests/crypto.test.ts +++ b/tests/crypto.test.ts @@ -1,4 +1,4 @@ -import { deriveKeyPair, hash, sign, verify, ecDecrypt, ecEncrypt } from "../src/crypto"; +import { deriveKeyPair, ecDecrypt, ecEncrypt, encryptSecret, hash, sign, verify } from "../src/crypto"; import { uint8ArrayToHex } from "../src/utils"; import { Curve, HashAlgorithm } from "../src/types"; @@ -115,4 +115,70 @@ describe("crypto", () => { expect(ecDecrypt(ciphertext, keypair.privateKey)).toStrictEqual(blob); }); }); + + describe("encryptSecret", () => { + it("should encrypt a secret using a public key", () => { + const keypair = deriveKeyPair("seed", 0); + const secret = "mySecret"; + const publicKey = uint8ArrayToHex(keypair.publicKey); + const result = encryptSecret(secret, publicKey); + + expect(result).toStrictEqual({ + encryptedSecret: result.encryptedSecret, + authorizedKeys: [ + { + publicKey: result.authorizedKeys[0].publicKey, + encryptedSecretKey: result.authorizedKeys[0].encryptedSecretKey + } + ] + }); + }); + + it("should return an object with encryptedSecret and authorizedKeys", () => { + const keypair = deriveKeyPair("seed", 0); + const secret = "mySecret"; + const publicKey = uint8ArrayToHex(keypair.publicKey); + const result = encryptSecret(secret, publicKey); + + expect(result).toHaveProperty("encryptedSecret"); + expect(result).toHaveProperty("authorizedKeys"); + expect(result.authorizedKeys[0]).toHaveProperty("encryptedSecretKey"); + expect(result.authorizedKeys[0]).toHaveProperty("publicKey"); + }); + + it("should return different results for different secrets", () => { + const keypair = deriveKeyPair("seed", 0); + const secret1 = "mySecret1"; + const secret2 = "mySecret2"; + const publicKey = uint8ArrayToHex(keypair.publicKey); + const result1 = encryptSecret(secret1, publicKey); + const result2 = encryptSecret(secret2, publicKey); + + expect(result1.encryptedSecret).not.toEqual(result2.encryptedSecret); + }); + + it("should return different results for different public keys", () => { + const keypair1 = deriveKeyPair("seed", 0); + const keypair2 = deriveKeyPair("seed2", 0); + const secret = "mySecret"; + const publicKey1 = uint8ArrayToHex(keypair1.publicKey); + const publicKey2 = uint8ArrayToHex(keypair2.publicKey); + const result1 = encryptSecret(secret, publicKey1); + const result2 = encryptSecret(secret, publicKey2); + + expect(result1.authorizedKeys[0].encryptedSecretKey).not.toEqual(result2.authorizedKeys[0].encryptedSecretKey); + }); + + it("should return the diferent result with different curve", () => { + const keypair1 = deriveKeyPair("seed", 0, Curve.ed25519); + const keypair2 = deriveKeyPair("seed", 0, Curve.P256); + const secret = "mySecret"; + const publicKey1 = uint8ArrayToHex(keypair1.publicKey); + const publicKey2 = uint8ArrayToHex(keypair2.publicKey); + const result1 = encryptSecret(secret, publicKey1); + const result2 = encryptSecret(secret, publicKey2); + + expect(result1.authorizedKeys[0].encryptedSecretKey).not.toEqual(result2.authorizedKeys[0].encryptedSecretKey); + }); + }); }); From ebd49eddfd53f12596ce3b8677b02e50199391a1 Mon Sep 17 00:00:00 2001 From: Rudy-Perrin Date: Fri, 26 Apr 2024 00:09:09 +0200 Subject: [PATCH 05/10] refactor(crypto): improve imports, typing and JSDoc --- package-lock.json | 44 ++++++++++++++++--- package.json | 2 + src/crypto.ts | 109 +++++++++++++++++++++++++++++----------------- 3 files changed, 110 insertions(+), 45 deletions(-) diff --git a/package-lock.json b/package-lock.json index d5b60a1f..97b6b2be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,8 +27,10 @@ }, "devDependencies": { "@types/absinthe__socket": "^0.2.3", + "@types/ed2curve": "^0.2.4", "@types/elliptic": "^6.4.14", "@types/jest": "^29.5.0", + "@types/sjcl": "^1.0.34", "esbuild": "^0.19.0", "jest": "^29.5.0", "nock": "^13.3.0", @@ -1525,6 +1527,15 @@ "@types/node": "*" } }, + "node_modules/@types/ed2curve": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@types/ed2curve/-/ed2curve-0.2.4.tgz", + "integrity": "sha512-1m9IX8qypOa0K1NfdMsMxB3gjhKyXbr5Yl9FzzwWQjLSDFGLkvRvSfla1NYqzIt72ocIALMXsF+twRlzr1ov/g==", + "dev": true, + "dependencies": { + "tweetnacl": "^1.0.0" + } + }, "node_modules/@types/elliptic": { "version": "6.4.14", "resolved": "https://registry.npmjs.org/@types/elliptic/-/elliptic-6.4.14.tgz", @@ -1589,6 +1600,12 @@ "integrity": "sha512-g2/8Ogi2zfiS25jdGT5iDSo5yjruhhXaOuOJCkOxMW28w16VxFvjtAXjBNRo7WlRS4+UXAMj3mK46UwieNM/5g==", "dev": true }, + "node_modules/@types/sjcl": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/@types/sjcl/-/sjcl-1.0.34.tgz", + "integrity": "sha512-bQHEeK5DTQRunIfQeUMgtpPsNNCcZyQ9MJuAfW1I7iN0LDunTc78Fu17STbLMd7KiEY/g2zHVApippa70h6HoQ==", + "dev": true + }, "node_modules/@types/stack-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", @@ -2124,9 +2141,9 @@ } }, "node_modules/crypto-js": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz", - "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==" + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" }, "node_modules/curve25519-js": { "version": "0.0.4", @@ -5667,6 +5684,15 @@ "@types/node": "*" } }, + "@types/ed2curve": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@types/ed2curve/-/ed2curve-0.2.4.tgz", + "integrity": "sha512-1m9IX8qypOa0K1NfdMsMxB3gjhKyXbr5Yl9FzzwWQjLSDFGLkvRvSfla1NYqzIt72ocIALMXsF+twRlzr1ov/g==", + "dev": true, + "requires": { + "tweetnacl": "^1.0.0" + } + }, "@types/elliptic": { "version": "6.4.14", "resolved": "https://registry.npmjs.org/@types/elliptic/-/elliptic-6.4.14.tgz", @@ -5731,6 +5757,12 @@ "integrity": "sha512-g2/8Ogi2zfiS25jdGT5iDSo5yjruhhXaOuOJCkOxMW28w16VxFvjtAXjBNRo7WlRS4+UXAMj3mK46UwieNM/5g==", "dev": true }, + "@types/sjcl": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/@types/sjcl/-/sjcl-1.0.34.tgz", + "integrity": "sha512-bQHEeK5DTQRunIfQeUMgtpPsNNCcZyQ9MJuAfW1I7iN0LDunTc78Fu17STbLMd7KiEY/g2zHVApippa70h6HoQ==", + "dev": true + }, "@types/stack-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", @@ -6124,9 +6156,9 @@ } }, "crypto-js": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz", - "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==" + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" }, "curve25519-js": { "version": "0.0.4", diff --git a/package.json b/package.json index 67209782..c857cb10 100644 --- a/package.json +++ b/package.json @@ -31,8 +31,10 @@ "homepage": "https://github.com/archethic-foundation/libjs#readme", "devDependencies": { "@types/absinthe__socket": "^0.2.3", + "@types/ed2curve": "^0.2.4", "@types/elliptic": "^6.4.14", "@types/jest": "^29.5.0", + "@types/sjcl": "^1.0.34", "esbuild": "^0.19.0", "jest": "^29.5.0", "nock": "^13.3.0", diff --git a/src/crypto.ts b/src/crypto.ts index 1d873000..30c137da 100644 --- a/src/crypto.ts +++ b/src/crypto.ts @@ -16,21 +16,21 @@ import * as curve25519 from "curve25519-js"; import CryptoJS from "crypto-js"; import blake from "blakejs"; import nacl from "tweetnacl"; -import pkg from "elliptic"; -const { ec } = pkg; +import { ec } from "elliptic"; import sha3 from "js-sha3"; -// @ts-ignore import ed2curve from "ed2curve"; -// @ts-ignore import sjcl from "sjcl"; import Keychain from "./keychain.js"; const { sha3_512, sha3_256 } = sha3; -const EC = ec; -const ec_P256 = new EC("p256"); -const ec_secp256k1 = new EC("secp256k1"); +const ec_P256 = new ec("p256"); +const ec_secp256k1 = new ec("secp256k1"); const SOFTWARE_ID = 1; +/** + * Generate a random secret key of 32 bytes + * @returns {Uint8Array} Random secret key + */ export function randomSecretKey(): Uint8Array { return wordArrayToUint8Array(CryptoJS.lib.WordArray.random(32)); } @@ -46,6 +46,7 @@ const hashAlgoMap = { /** * Get the hash algo name from the hash algorithm ID * @param {number} ID Hash algorithm's ID + * @returns {HashAlgorithm} Hash algorithm's name */ export function IDToHashAlgo(ID: number): HashAlgorithm { const hashAlgo = Object.keys(hashAlgoMap).find((key) => hashAlgoMap[key as HashAlgorithm] === ID); @@ -57,7 +58,8 @@ export function IDToHashAlgo(ID: number): HashAlgorithm { /** * Get the ID of a given hash algorithm - * @params {String} hashAlgo Hash algorithm + * @param {String} hashAlgo Hash algorithm + * @returns {number} Hash algorithm's ID */ export function hashAlgoToID(hashAlgo: HashAlgorithm): number { const ID = hashAlgoMap[hashAlgo]; @@ -71,6 +73,7 @@ export function hashAlgoToID(hashAlgo: HashAlgorithm): number { * Get the hash digest of a given content for a given hash algorithm * @param {string | Uint8Array} content Content to hash * @param {HashAlgorithm} algo Hash algorithm + * @returns {Uint8Array} Hash digest */ export function getHashDigest(content: string | Uint8Array, algo: HashAlgorithm): Uint8Array { switch (algo) { @@ -108,7 +111,7 @@ export function getHashDigest(content: string | Uint8Array, algo: HashAlgorithm) * @param {HashAlgorithm} algo Hash algorithm to use * @returns {Uint8Array} Hash digest */ -export function hash(content: string | Uint8Array, algo: HashAlgorithm = HashAlgorithm.sha256) { +export function hash(content: string | Uint8Array, algo: HashAlgorithm = HashAlgorithm.sha256): Uint8Array { content = maybeStringToUint8Array(content); const algoID = hashAlgoToID(algo); @@ -119,7 +122,8 @@ export function hash(content: string | Uint8Array, algo: HashAlgorithm = HashAlg /** * Get the ID of a given Elliptic curve - * @params {String} curve Elliptic curve + * @param {String} curve Elliptic curve + * @returns {number} Curve's ID */ export function curveToID(curve: Curve): number { switch (curve) { @@ -140,6 +144,7 @@ export function curveToID(curve: Curve): number { /** * Get the curve name from the curve ID * @param {number} ID Curve's ID + * @returns {Curve} Curve's name */ export function IDToCurve(ID: number): Curve { switch (ID) { @@ -154,12 +159,18 @@ export function IDToCurve(ID: number): Curve { } } +/** + * Derive a private key from a seed and an index + * @param {string | Uint8Array} seed Seed to derive the private key + * @param {number} index Index to derive the private key + * @returns {Uint8Array} Derived private key + */ export function derivePrivateKey(seed: string | Uint8Array, index: number = 0): Uint8Array { - if (seed == undefined || seed == null) { + if (seed === undefined || seed === null) { throw new Error("Seed must be defined"); } - if (index == undefined || index == null) { + if (index === undefined || index === null) { throw new Error("Index must be defined"); } @@ -189,23 +200,24 @@ export function derivePrivateKey(seed: string | Uint8Array, index: number = 0): } /** - * Generate a keypair using a derivation function with a seed and an index. Each keys is prepending with a curve identification. + * Generate a keypair using a derivation function with a seed and an index. Each keys is prepending with a curve identification * @param {String} seed Keypair derivation seed * @param {number} index Number to identify the order of keys to generate - * @param {String} curve Elliptic curve to use ("ed25519", "P256", "secp256k1") + * @param {Curve} curve Elliptic curve to use ("ed25519", "P256", "secp256k1") * @param {number} origin_id Origin id of the public key (0, 1, 2) = ("on chain wallet", "software", "tpm") + * @returns {Object} {publicKey: Uint8Array, privateKey: Uint8Array} */ export function deriveKeyPair( seed: string | Uint8Array, index: number = 0, - curve = Curve.ed25519, + curve: Curve = Curve.ed25519, origin_id: number = SOFTWARE_ID ): Keypair { - if (seed == undefined || seed == null) { + if (seed === undefined || seed === null) { throw new Error("Seed must be defined"); } - if (index == undefined || index == null) { + if (index === undefined || index === null) { throw new Error("Index must be defined"); } @@ -218,13 +230,14 @@ export function deriveKeyPair( } /** - * Create an address from a seed, an index, an elliptic curve and an hash algorithm. - * The address is prepended by the curve identification, the hash algorithm and the digest of the address + * Create an address from a seed, an index, an elliptic curve and an hash algorithm * + * The address is prepended by the curve identification, the hash algorithm and the digest of the address * @param {string | Uint8Array} seed Keypair derivation seed * @param {number} index Number to identify the order of keys to generate * @param {Curve} curve Elliptic Curve to use * @param {HashAlgorithm} hashAlgo Hash algorithm to use + * @returns {Uint8Array} Address */ export function deriveAddress( seed: string | Uint8Array, @@ -243,9 +256,10 @@ export function deriveAddress( /** * Generate a new keypair deterministically with a given private key, curve and origin id - * @params {Uint8Array} privateKey Private key - * @params {String} curve Elliptic curve - * @params {Integer} originID Origin identification + * @param {Uint8Array} pvKey Private key + * @param {String} curve Elliptic curve + * @param {Integer} originID Origin identification + * @returns {Object} {publicKey: Uint8Array, privateKey: Uint8Array} */ export function generateDeterministicKeyPair(pvKey: string | Uint8Array, curve: Curve, originID: number): Keypair { if (typeof pvKey === "string") { @@ -315,6 +329,7 @@ function getKeypair(pvKey: string | Uint8Array, curve: Curve): { publicKey: Uint * Sign data with a private key * @param { string | Uint8Array } data Data to sign * @param { string | Uint8Array } privateKey Private key used to sign the data + * @returns { Uint8Array } Signature */ export function sign(data: string | Uint8Array, privateKey: string | Uint8Array): Uint8Array { privateKey = maybeStringToUint8Array(privateKey); @@ -352,6 +367,7 @@ export function sign(data: string | Uint8Array, privateKey: string | Uint8Array) * @param {string | Uint8Array} sig Signature to verify * @param {string | Uint8Array} data Data to verify * @param {string | Uint8Array} publicKey Public key used to verify the signature + * @returns {boolean} True if the signature is valid, false otherwise */ export function verify(sig: string | Uint8Array, data: string | Uint8Array, publicKey: string | Uint8Array): boolean { sig = maybeStringToUint8Array(sig); @@ -387,6 +403,7 @@ export function verify(sig: string | Uint8Array, data: string | Uint8Array, publ * Encrypt a data for a given public key using ECIES algorithm * @param {string | Uint8Array} data Data to encrypt * @param {string | Uint8Array} publicKey Public key for the shared secret encryption + * @returns {Uint8Array} Encrypted data */ export function ecEncrypt(data: string | Uint8Array, publicKey: string | Uint8Array): Uint8Array { publicKey = maybeStringToUint8Array(publicKey); @@ -437,6 +454,12 @@ export function ecEncrypt(data: string | Uint8Array, publicKey: string | Uint8Ar } } +/** + * Decrypt a data for a given private key using ECIES algorithm + * @param {string | Uint8Array} ciphertext Data to decrypt + * @param {string | Uint8Array} privateKey Private key for the shared secret decryption + * @returns {Uint8Array} Decrypted data + */ export function ecDecrypt(ciphertext: string | Uint8Array, privateKey: string | Uint8Array): Uint8Array { ciphertext = maybeStringToUint8Array(ciphertext); privateKey = maybeStringToUint8Array(privateKey); @@ -492,7 +515,8 @@ export function ecDecrypt(ciphertext: string | Uint8Array, privateKey: string | /** * Encrypt a data for a given public key using AES algorithm * @param {string | Uint8Array} data Data to encrypt - * @param {string | Uint8Array} key Symmetric key + * @param {string | Uint8Array} key Public key for the shared secret encryption + * @returns {Uint8Array} Encrypted data */ export function aesEncrypt(data: string | Uint8Array, key: string | Uint8Array): Uint8Array { key = maybeHexToUint8Array(key); @@ -506,9 +530,10 @@ export function aesEncrypt(data: string | Uint8Array, key: string | Uint8Array): } /** - * Decrypt cipherText for a given key using AES algorithm - * @param cipherText Ciphertext to decrypt - * @param key Symmetric key + * Decrypt a data for a given private key using AES algorithm + * @param {string | Uint8Array} cipherText Data to decrypt + * @param {string | Uint8Array} key Private key for the shared secret decryption + * @returns {Uint8Array} Decrypted data */ export function aesDecrypt(cipherText: string | Uint8Array, key: string | Uint8Array): Uint8Array { cipherText = maybeHexToUint8Array(cipherText); @@ -523,7 +548,8 @@ export function aesDecrypt(cipherText: string | Uint8Array, key: string | Uint8A /** * Derive a secret from a shared key - * @param sharedKey + * @param {Uint8Array} sharedKey + * @returns {Object} {aesKey: Uint8Array, iv: Uint8Array} */ function deriveSecret(sharedKey: Uint8Array): { aesKey: Uint8Array; iv: Uint8Array } { sharedKey = CryptoJS.lib.WordArray.create(sharedKey); @@ -537,9 +563,10 @@ function deriveSecret(sharedKey: Uint8Array): { aesKey: Uint8Array; iv: Uint8Arr /** * Encrypt data with AES - * @param data Data to encrypt - * @param aesKey AES key - * @param iv Initialization vector + * @param {Uint8Array} data Data to encrypt + * @param {Uint8Array} aesKey AES key + * @param {Uint8Array} iv Initialization vector + * @returns {Object} {tag: Uint8Array, encrypted: Uint8Array} */ function aesAuthEncrypt( data: Uint8Array, @@ -551,6 +578,7 @@ function aesAuthEncrypt( const dataBits = sjcl.codec.hex.toBits(uint8ArrayToHex(data)); const ivBits = sjcl.codec.hex.toBits(uint8ArrayToHex(iv)); + // @ts-expect-error sjcl.mode.gcm.C is not in types const { tag, data: encrypted } = sjcl.mode.gcm.C(true, new sjcl.cipher.aes(keyBits), dataBits, [], ivBits, 128); return { @@ -561,18 +589,20 @@ function aesAuthEncrypt( /** * Decrypt data with AES - * @param encrypted Encrypted data - * @param aesKey AES key - * @param iv Initialization vector - * @param tag Tag + * @param {Uint8Array} encrypted Encrypted data + * @param {Uint8Array} aesKey AES key + * @param {Uint8Array} iv Initialization vector + * @param {Uint8Array} tag Tag + * @returns {Uint8Array} Decrypted data */ -function aesAuthDecrypt(encrypted: Uint8Array, aesKey: Uint8Array, iv: Uint8Array, tag: Uint8Array) { +function aesAuthDecrypt(encrypted: Uint8Array, aesKey: Uint8Array, iv: Uint8Array, tag: Uint8Array): Uint8Array { // Format for SJCL const encryptedBits = sjcl.codec.hex.toBits(uint8ArrayToHex(encrypted)); const aesKeyBits = sjcl.codec.hex.toBits(uint8ArrayToHex(aesKey)); const ivBits = sjcl.codec.hex.toBits(uint8ArrayToHex(iv)); const tagBits = sjcl.codec.hex.toBits(uint8ArrayToHex(tag)); + // @ts-expect-error sjcl.mode.gcm.C is not in types const { tag: actualTag, data: decrypted } = sjcl.mode.gcm.C( false, new sjcl.cipher.aes(aesKeyBits), @@ -589,10 +619,10 @@ function aesAuthDecrypt(encrypted: Uint8Array, aesKey: Uint8Array, iv: Uint8Arra /** * Generates the genesis address (the first address) from a given seed - * @param {string | Uint8Array} seed The seed used to generate the address. Can be a string or a Uint8Array + * @param {string | Uint8Array} seed The seed used to generate the address * @returns {string} The genesis address in hexadecimal format */ -export function getGenesisAddress(seed: string | Uint8Array) { +export function getGenesisAddress(seed: string | Uint8Array): string { return uint8ArrayToHex(deriveAddress(seed, 0)); } @@ -603,14 +633,15 @@ export function getGenesisAddress(seed: string | Uint8Array) { * @param {string} [suffix=""] An optional suffix to append to the service before deriving the address * @returns {string} The genesis address for the service in hexadecimal format */ -export function getServiceGenesisAddress(keychain: Keychain, service: string, suffix = "") { +export function getServiceGenesisAddress(keychain: Keychain, service: string, suffix: string = ""): string { return uint8ArrayToHex(keychain.deriveAddress(service, 0, suffix)); } + /** * Encrypts a secret using a given public key * @param {string | Uint8Array} secret The secret to encrypt * @param {string | Uint8Array} publicKey The public key to use for encryption - * @returns {Object} An object containing the encrypted secret and an array of authorized keys, each with an encrypted secret key and a public key + * @returns {Object} {encryptedSecret: Uint8Array, authorizedKeys: AuthorizedKeyUserInput[]} * @example * const storageNoncePublicKey = await archethic.network.getStorageNoncePublicKey(); * const { encryptedSecret, authorizedKeys } = encryptSecret(Crypto.randomSecretKey(), storageNoncePublicKey); From 27aca155ac21a361a6aec55327568f6662805bc6 Mon Sep 17 00:00:00 2001 From: Rudy-Perrin Date: Mon, 29 Apr 2024 23:45:55 +0200 Subject: [PATCH 06/10] refactor(crypto): improved clarity and consistency of aesDecrypt and aesEncrypt --- src/crypto.ts | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/crypto.ts b/src/crypto.ts index 30c137da..8548bd48 100644 --- a/src/crypto.ts +++ b/src/crypto.ts @@ -513,37 +513,36 @@ export function ecDecrypt(ciphertext: string | Uint8Array, privateKey: string | } /** - * Encrypt a data for a given public key using AES algorithm + * Encrypt a data for a given AES key using AES algorithm * @param {string | Uint8Array} data Data to encrypt - * @param {string | Uint8Array} key Public key for the shared secret encryption + * @param {string | Uint8Array} aesKey AES key (Symmetric key) * @returns {Uint8Array} Encrypted data */ -export function aesEncrypt(data: string | Uint8Array, key: string | Uint8Array): Uint8Array { - key = maybeHexToUint8Array(key); +export function aesEncrypt(data: string | Uint8Array, aesKey: string | Uint8Array): Uint8Array { + aesKey = maybeHexToUint8Array(aesKey); data = maybeStringToUint8Array(data); const iv = wordArrayToUint8Array(CryptoJS.lib.WordArray.random(12)); - - const { tag, encrypted } = aesAuthEncrypt(data, key, iv); + const { tag, encrypted } = aesAuthEncrypt(data, aesKey, iv); return concatUint8Arrays(new Uint8Array(iv), tag, encrypted); } /** - * Decrypt a data for a given private key using AES algorithm + * Decrypt a data for a given AES key using AES algorithm * @param {string | Uint8Array} cipherText Data to decrypt - * @param {string | Uint8Array} key Private key for the shared secret decryption + * @param {string | Uint8Array} aesKey AES key (Symmetric key) * @returns {Uint8Array} Decrypted data */ -export function aesDecrypt(cipherText: string | Uint8Array, key: string | Uint8Array): Uint8Array { +export function aesDecrypt(cipherText: string | Uint8Array, aesKey: string | Uint8Array): Uint8Array { cipherText = maybeHexToUint8Array(cipherText); - key = maybeHexToUint8Array(key); + aesKey = maybeHexToUint8Array(aesKey); const iv = cipherText.slice(0, 12); const tag = cipherText.slice(12, 12 + 16); const encrypted = cipherText.slice(28, cipherText.length); - return aesAuthDecrypt(encrypted, key, iv, tag); + return aesAuthDecrypt(encrypted, aesKey, iv, tag); } /** From 00a4820e052506664e6ad0846c98a1914230b33d Mon Sep 17 00:00:00 2001 From: Rudy Perrin <15348015+Rudy-Perrin@users.noreply.github.com> Date: Fri, 24 May 2024 23:48:33 +0200 Subject: [PATCH 07/10] Update src/crypto.ts Co-authored-by: bchamagne <74045243+bchamagne@users.noreply.github.com> --- src/crypto.ts | 47 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/src/crypto.ts b/src/crypto.ts index 26884826..f517bccc 100644 --- a/src/crypto.ts +++ b/src/crypto.ts @@ -700,11 +700,11 @@ export function getServiceGenesisAddress(keychain: Keychain, service: string, su /** * Encrypts a secret using a given public key * @param {string | Uint8Array} secret The secret to encrypt - * @param {string | Uint8Array} publicKey The public key to use for encryption - * @returns {Object} {encryptedSecret: Uint8Array, authorizedKeys: AuthorizedKeyUserInput[]} + * @param {string | string[] | Uint8Array | Uint8Array[]} publicKeys The public keys authorized to decrypt the secret + * @returns {{encryptedSecret: Uint8Array, authorizedKeys: AuthorizedKeyUserInput[]}} * @example * const storageNoncePublicKey = await archethic.network.getStorageNoncePublicKey(); - * const { encryptedSecret, authorizedKeys } = encryptSecret(Crypto.randomSecretKey(), storageNoncePublicKey); + * const { encryptedSecret, authorizedKeys } = encryptSecret("something secret", storageNoncePublicKey); * const code = "" // The contract code * const tx = await archethic.transaction * .new() @@ -717,13 +717,44 @@ export function getServiceGenesisAddress(keychain: Keychain, service: string, su */ export function encryptSecret( secret: string | Uint8Array, - publicKey: string | Uint8Array + publicKeys: string | string[] | Uint8Array | Uint8Array[] ): { encryptedSecret: Uint8Array; authorizedKeys: AuthorizedKeyUserInput[] } { + const publicKeysWrapped = + typeof publicKeys == "string" || publicKeys instanceof Uint8Array ? [publicKeys] : publicKeys; + const aesKey = randomSecretKey(); const encryptedSecret = aesEncrypt(secret, aesKey); - const encryptedAesKey = uint8ArrayToHex(ecEncrypt(aesKey, publicKey)); - const authorizedKeys: AuthorizedKeyUserInput[] = [ - { encryptedSecretKey: encryptedAesKey, publicKey: maybeUint8ArrayToHex(publicKey) } - ]; + + const authorizedKeys: AuthorizedKeyUserInput[] = publicKeysWrapped.map((publicKey) => { + const encryptedAesKey = uint8ArrayToHex(ecEncrypt(aesKey, publicKey)); + return { encryptedSecretKey: encryptedAesKey, publicKey: maybeUint8ArrayToHex(publicKey) }; + }); + return { encryptedSecret, authorizedKeys }; } + +/** + * Decrypt a secret if authorized + * @param {Uint8Array} encryptedSecret The secret to decrypt + * @param {AuthorizedKeyUserInput[]} authorizedKeys The AES keys of authorized keys + * @param {Keypair} keyPair The keyPair to use for decrypting + * @returns {Uint8Array} The decrypted secret + * @example + * const keypair = deriveKeyPair("seed", 0); + * const { encryptedSecret, authorizedKeys } = encryptSecret("something secret", keypair.publicKey); + * const decryptedSecret = decryptSecret(encryptedSecret, authorizedKeys, keypair); + */ +export function decryptSecret( + encryptedSecret: Uint8Array, + authorizedKeys: AuthorizedKeyUserInput[], + keyPair: Keypair +): Uint8Array { + const publicKeyInHex = uint8ArrayToHex(keyPair.publicKey); + + const authorizedKey = authorizedKeys.find(({ publicKey: currentPubKey }) => currentPubKey == publicKeyInHex); + if (!authorizedKey) throw new Error("This keypair is not authorized to decrypt the secret"); + + const aesKey = ecDecrypt(authorizedKey.encryptedSecretKey, keyPair.privateKey); + return aesDecrypt(encryptedSecret, aesKey); +} + From 4b3b8881734aa87292ce471582431d5528fa3183 Mon Sep 17 00:00:00 2001 From: Rudy Perrin <15348015+Rudy-Perrin@users.noreply.github.com> Date: Fri, 24 May 2024 23:49:06 +0200 Subject: [PATCH 08/10] Update tests/crypto.test.ts Co-authored-by: bchamagne <74045243+bchamagne@users.noreply.github.com> --- tests/crypto.test.ts | 51 +++++++++++++++++++++++++++++++++----------- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/tests/crypto.test.ts b/tests/crypto.test.ts index 96a0eda7..2f9f5fee 100644 --- a/tests/crypto.test.ts +++ b/tests/crypto.test.ts @@ -173,22 +173,47 @@ describe("crypto", () => { }); }); - describe("encryptSecret", () => { - it("should encrypt a secret using a public key", () => { + describe("encryptSecret / decryptSecret", () => { + it("should encrypt a secret (string) using a public key and then decrypt it", () => { const keypair = deriveKeyPair("seed", 0); const secret = "mySecret"; - const publicKey = uint8ArrayToHex(keypair.publicKey); - const result = encryptSecret(secret, publicKey); + const result = encryptSecret(secret, keypair.publicKey); + + const secretDecrypted = decryptSecret(result.encryptedSecret, result.authorizedKeys, keypair); + expect(new TextDecoder().decode(secretDecrypted)).toStrictEqual(secret); + }); + + it("should encrypt a secret (uint8array) using a public key and then decrypt it", () => { + const keypair = deriveKeyPair("seed", 0); + const secret = new TextEncoder().encode("mySecret"); + const result = encryptSecret(secret, keypair.publicKey); + + const secretDecrypted = decryptSecret(result.encryptedSecret, result.authorizedKeys, keypair); + expect(secretDecrypted).toStrictEqual(secret); + }); + + it("should be able to be decrypted by anyone authorized", () => { + const keypair1 = deriveKeyPair("seed", 0); + const keypair2 = deriveKeyPair("seed2", 0); + const secret = new TextEncoder().encode("mySecret"); + const result = encryptSecret(secret, [keypair1.publicKey, keypair2.publicKey]); + + const secretDecrypted1 = decryptSecret(result.encryptedSecret, result.authorizedKeys, keypair1); + const secretDecrypted2 = decryptSecret(result.encryptedSecret, result.authorizedKeys, keypair2); + expect(secretDecrypted1).toStrictEqual(secret); + expect(secretDecrypted2).toStrictEqual(secret); + }); + + it("should not be able to be decrypted by non-authorized keys", () => { + const keypair1 = deriveKeyPair("seed", 0); + const keypair2 = deriveKeyPair("seed2", 0); + const secret = "mySecret"; + const publicKey = uint8ArrayToHex(keypair1.publicKey); + const result = encryptSecret(secret, [publicKey]); - expect(result).toStrictEqual({ - encryptedSecret: result.encryptedSecret, - authorizedKeys: [ - { - publicKey: result.authorizedKeys[0].publicKey, - encryptedSecretKey: result.authorizedKeys[0].encryptedSecretKey - } - ] - }); + expect(() => { + decryptSecret(result.encryptedSecret, result.authorizedKeys, keypair2); + }).toThrow(); }); it("should return an object with encryptedSecret and authorizedKeys", () => { From a3c87921658eb84366bd268a8478c1bc23263e09 Mon Sep 17 00:00:00 2001 From: Rudy Perrin <15348015+Rudy-Perrin@users.noreply.github.com> Date: Fri, 24 May 2024 23:49:14 +0200 Subject: [PATCH 09/10] Update tests/crypto.test.ts Co-authored-by: bchamagne <74045243+bchamagne@users.noreply.github.com> --- tests/crypto.test.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/crypto.test.ts b/tests/crypto.test.ts index 2f9f5fee..c0b816d4 100644 --- a/tests/crypto.test.ts +++ b/tests/crypto.test.ts @@ -1,4 +1,14 @@ -import { deriveKeyPair, ecDecrypt, ecEncrypt, isValidAddress, encryptSecret, hash, sign, verify } from "../src/crypto"; +import { + deriveKeyPair, + ecDecrypt, + ecEncrypt, + isValidAddress, + encryptSecret, + hash, + sign, + verify, + decryptSecret +} from "../src/crypto"; import { uint8ArrayToHex } from "../src/utils"; import { Curve, HashAlgorithm } from "../src/types"; From 17acc4320db5b38951a38f9c876306c840224a12 Mon Sep 17 00:00:00 2001 From: Samuel Manzanera Date: Tue, 11 Jun 2024 15:33:07 +0200 Subject: [PATCH 10/10] Use spread operators in encryptSecret for public keys parameters --- src/crypto.ts | 10 +++------- tests/crypto.test.ts | 4 ++-- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/crypto.ts b/src/crypto.ts index f517bccc..0ac94076 100644 --- a/src/crypto.ts +++ b/src/crypto.ts @@ -700,7 +700,7 @@ export function getServiceGenesisAddress(keychain: Keychain, service: string, su /** * Encrypts a secret using a given public key * @param {string | Uint8Array} secret The secret to encrypt - * @param {string | string[] | Uint8Array | Uint8Array[]} publicKeys The public keys authorized to decrypt the secret + * @param {string[] | Uint8Array[]} publicKeys The public keys authorized to decrypt the secret * @returns {{encryptedSecret: Uint8Array, authorizedKeys: AuthorizedKeyUserInput[]}} * @example * const storageNoncePublicKey = await archethic.network.getStorageNoncePublicKey(); @@ -717,15 +717,12 @@ export function getServiceGenesisAddress(keychain: Keychain, service: string, su */ export function encryptSecret( secret: string | Uint8Array, - publicKeys: string | string[] | Uint8Array | Uint8Array[] + ...publicKeys: string[] | Uint8Array[] ): { encryptedSecret: Uint8Array; authorizedKeys: AuthorizedKeyUserInput[] } { - const publicKeysWrapped = - typeof publicKeys == "string" || publicKeys instanceof Uint8Array ? [publicKeys] : publicKeys; - const aesKey = randomSecretKey(); const encryptedSecret = aesEncrypt(secret, aesKey); - const authorizedKeys: AuthorizedKeyUserInput[] = publicKeysWrapped.map((publicKey) => { + const authorizedKeys: AuthorizedKeyUserInput[] = publicKeys.map((publicKey) => { const encryptedAesKey = uint8ArrayToHex(ecEncrypt(aesKey, publicKey)); return { encryptedSecretKey: encryptedAesKey, publicKey: maybeUint8ArrayToHex(publicKey) }; }); @@ -757,4 +754,3 @@ export function decryptSecret( const aesKey = ecDecrypt(authorizedKey.encryptedSecretKey, keyPair.privateKey); return aesDecrypt(encryptedSecret, aesKey); } - diff --git a/tests/crypto.test.ts b/tests/crypto.test.ts index c0b816d4..7ae003ec 100644 --- a/tests/crypto.test.ts +++ b/tests/crypto.test.ts @@ -206,7 +206,7 @@ describe("crypto", () => { const keypair1 = deriveKeyPair("seed", 0); const keypair2 = deriveKeyPair("seed2", 0); const secret = new TextEncoder().encode("mySecret"); - const result = encryptSecret(secret, [keypair1.publicKey, keypair2.publicKey]); + const result = encryptSecret(secret, keypair1.publicKey, keypair2.publicKey); const secretDecrypted1 = decryptSecret(result.encryptedSecret, result.authorizedKeys, keypair1); const secretDecrypted2 = decryptSecret(result.encryptedSecret, result.authorizedKeys, keypair2); @@ -219,7 +219,7 @@ describe("crypto", () => { const keypair2 = deriveKeyPair("seed2", 0); const secret = "mySecret"; const publicKey = uint8ArrayToHex(keypair1.publicKey); - const result = encryptSecret(secret, [publicKey]); + const result = encryptSecret(secret, ...[publicKey]); expect(() => { decryptSecret(result.encryptedSecret, result.authorizedKeys, keypair2);