diff --git a/contracts/package.json b/contracts/package.json index 19dcbdfc18..b929ca410a 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -53,6 +53,7 @@ "@openzeppelin/defender-relay-client": "^1.54.4", "@openzeppelin/defender-sdk": "^1.13.1", "@openzeppelin/hardhat-upgrades": "^1.10.0", + "@rigidity/bls-signatures": "^2.0.5", "@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^15.2.3", diff --git a/contracts/tasks/crypto.js b/contracts/tasks/crypto.js new file mode 100644 index 0000000000..b4c038880b --- /dev/null +++ b/contracts/tasks/crypto.js @@ -0,0 +1,104 @@ +const bls = require("@rigidity/bls-signatures"); +const { + subtle, + createCipheriv, + createECDH, + createHash, + createHmac, +} = require("node:crypto"); + +const ecdhCurveName = "prime256v1"; + +const genECDHKey = async ({ privateKey }) => { + const ecdh = createECDH(ecdhCurveName); + + if (privateKey) { + ecdh.setPrivateKey(Buffer.from(privateKey, "hex")); + } else { + ecdh.generateKeys(); + } + + const publicKeyBase64 = ecdh.getPublicKey("base64"); + + console.log(`Public key: ${publicKeyBase64}`); + + const subtleKey = await subtle.importKey( + "raw", + ecdh.getPublicKey(), + { name: "ECDH", namedCurve: "P-256" }, + true, + [] + ); + const fmtKey = await subtle.exportKey("spki", subtleKey); + const publicKeyPEM = `-----BEGIN PUBLIC KEY-----\n${Buffer.from(fmtKey) + .toString("base64") + .replace(/.{64}/g, "$&\n") + .replace(/\n$/g, "")}\n-----END PUBLIC KEY-----\n`; + console.log(`Public key in PEM format:\n${publicKeyPEM.toString()}`); + + // base64 encode the PEM format again to get P2P format + const p2pPublicKey = Buffer.from(publicKeyPEM).toString("base64"); + console.log(`Encoded public key for P2P API:\n${p2pPublicKey}`); +}; + +const decryptValidatorKey = async ({ privateKey, message }) => { + const ecdh = createECDH(ecdhCurveName); + ecdh.setPrivateKey(privateKey, "hex"); + + const validatorPrivateKey = decrypt(ecdh, Buffer.from(message, "base64")); + + const vsk = bls.PrivateKey.fromBytes(validatorPrivateKey); + console.log(`Validator public key: ${vsk.getG1().toHex()}`); +}; + +const decrypt = (ecdh, msg) => { + const epk = msg.slice(0, 65); + const message = msg.slice(65, msg.length - 32); + const sharedSecret = ecdh.computeSecret(epk); + const { encKey, macKey } = deriveKeys(sharedSecret, Buffer.alloc(0), 16); + const tag = messageTag(macKey, message, Buffer.alloc(0)); + if (tag.toString("hex") !== msg.slice(msg.length - 32).toString("hex")) { + throw new Error("tag mismatch"); + } + return symDecrypt(encKey, message); +}; + +const deriveKeys = (secret, s1, keyLen) => { + const keys = concatKDF(secret, s1, keyLen * 2); + const encKey = keys.slice(0, keyLen); + const macKey = createHash("sha256") + .update(keys.slice(keyLen, keyLen * 2)) + .digest(); + return { encKey, macKey }; +}; + +const messageTag = (macKey, message, s2) => { + return createHmac("sha256", macKey).update(message).update(s2).digest(); +}; + +const symDecrypt = (key, ct) => { + const c = createCipheriv("aes-128-ctr", key, ct.slice(0, 16)); + const m = Buffer.alloc(ct.length - 16); + c.update(ct.slice(16)).copy(m); + return m; +}; + +const concatKDF = (secret, s1, keyLen) => { + let hashSum = Buffer.from(""); + for (let ctr = 1; hashSum.length < keyLen; ctr++) { + const ctrs = Buffer.from([ctr >> 24, ctr >> 16, ctr >> 8, ctr]); // Buffer.from([ctr >> 24, ctr >> 16, ctr >> 8, ctr]) + const tmp = [ + hashSum, + createHash("sha256") + .update(Buffer.concat([ctrs, secret, s1])) + .digest(), + ]; + hashSum = Buffer.concat(tmp); + } + return hashSum.slice(0, keyLen); +}; + +module.exports = { + genECDHKey, + decryptValidatorKey, +}; diff --git a/contracts/tasks/tasks.js b/contracts/tasks/tasks.js index 720a43ace0..436ddf7b80 100644 --- a/contracts/tasks/tasks.js +++ b/contracts/tasks/tasks.js @@ -7,6 +7,8 @@ const { execute, executeOnFork, proposal, governors } = require("./governance"); const { smokeTest, smokeTestCheck } = require("./smokeTest"); const addresses = require("../utils/addresses"); const { networkMap } = require("../utils/hardhat-helpers"); +const { resolveContract } = require("../utils/resolvers"); +const { genECDHKey, decryptValidatorKey } = require("./crypto.js"); const { getSigner } = require("../utils/signers"); const { @@ -81,7 +83,6 @@ const { fixAccounting, pauseStaking, } = require("./validator"); -const { resolveContract } = require("../utils/resolvers"); const { harvestAndSwap } = require("./harvest"); // can not import from utils/deploy since that imports hardhat globally @@ -1169,6 +1170,41 @@ task("pauseStaking").setAction(async (_, __, runSuper) => { return runSuper(); }); +// Encryption + +subtask("genECDHKey", "Generate Elliptic-curve Diffie–Hellman (ECDH) key pair") + .addOptionalParam( + "privateKey", + "Private key to encrypt the message with in base64 format", + undefined, + types.string + ) + .setAction(genECDHKey); +task("genECDHKey").setAction(async (_, __, runSuper) => { + return runSuper(); +}); + +subtask( + "decrypt", + "Decrypt a message using a Elliptic-curve Diffie–Hellman (ECDH) key pair" +) + .addParam( + "privateKey", + "Private key to decrypt the message with in hex format without the 0x prefix", + undefined, + types.string + ) + .addParam( + "message", + "Encrypted validator key returned form P2P API", + undefined, + types.string + ) + .setAction(decryptValidatorKey); +task("decrypt").setAction(async (_, __, runSuper) => { + return runSuper(); +}); + // Defender subtask( "setActionVars", diff --git a/contracts/yarn.lock b/contracts/yarn.lock index 5e0638a4f1..38bcc30b88 100644 --- a/contracts/yarn.lock +++ b/contracts/yarn.lock @@ -1294,6 +1294,15 @@ path-browserify "^1.0.0" url "^0.11.0" +"@rigidity/bls-signatures@^2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@rigidity/bls-signatures/-/bls-signatures-2.0.5.tgz#4bd32a4594bb0c61a54101e4e1736ffc4de17e49" + integrity sha512-ybuB+z1+Duyy7wuyBbuM0xRc+pEbMqDQ7UjjkzceYQRWg78RmsFcNs0tNvZbmThYc/IFvb7sWmdgzbOcvtevYA== + dependencies: + chai "^4.3.6" + jssha "^3.2.0" + randombytes "^2.1.0" + "@rollup/plugin-commonjs@^25.0.7": version "25.0.7" resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.7.tgz#145cec7589ad952171aeb6a585bbeabd0fd3b4cf" @@ -2757,6 +2766,19 @@ chai@^4.3.4: pathval "^1.1.1" type-detect "^4.0.5" +chai@^4.3.6: + version "4.4.1" + resolved "https://registry.yarnpkg.com/chai/-/chai-4.4.1.tgz#3603fa6eba35425b0f2ac91a009fe924106e50d1" + integrity sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g== + dependencies: + assertion-error "^1.1.0" + check-error "^1.0.3" + deep-eql "^4.1.3" + get-func-name "^2.0.2" + loupe "^2.3.6" + pathval "^1.1.1" + type-detect "^4.0.8" + chalk@^2.0.0, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -2789,6 +2811,13 @@ check-error@^1.0.2: resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" integrity sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA== +check-error@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.3.tgz#a6502e4312a7ee969f646e83bb3ddd56281bd694" + integrity sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg== + dependencies: + get-func-name "^2.0.2" + chokidar@3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.3.0.tgz#12c0714668c55800f659e262d4962a97faf554a6" @@ -3311,7 +3340,7 @@ decompress-response@^6.0.0: dependencies: mimic-response "^3.1.0" -deep-eql@^4.1.2: +deep-eql@^4.1.2, deep-eql@^4.1.3: version "4.1.3" resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-4.1.3.tgz#7c7775513092f7df98d8df9996dd085eb668cc6d" integrity sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw== @@ -4694,6 +4723,11 @@ get-func-name@^2.0.0: resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41" integrity sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig== +get-func-name@^2.0.1, get-func-name@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.2.tgz#0d7cf20cd13fda808669ffa88f4ffc7a3943fc41" + integrity sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ== + get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0: version "1.2.1" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82" @@ -5886,6 +5920,11 @@ jsprim@^1.2.2: json-schema "0.4.0" verror "1.10.0" +jssha@^3.2.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/jssha/-/jssha-3.3.1.tgz#c5b7fc7fb9aa745461923b87df0e247dd59c7ea8" + integrity sha512-VCMZj12FCFMQYcFLPRm/0lOBbLi8uM2BhXPTqw3U4YAfs4AZfiApOoBLoN8cQE60Z50m1MYMTQVCfgF/KaCVhQ== + jszip@^3.10.1, jszip@^3.5.0: version "3.10.1" resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.10.1.tgz#34aee70eb18ea1faec2f589208a157d1feb091c2" @@ -6164,6 +6203,13 @@ loupe@^2.3.1: dependencies: get-func-name "^2.0.0" +loupe@^2.3.6: + version "2.3.7" + resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.7.tgz#6e69b7d4db7d3ab436328013d37d1c8c3540c697" + integrity sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA== + dependencies: + get-func-name "^2.0.1" + lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" @@ -8598,7 +8644,7 @@ type-check@~0.3.2: dependencies: prelude-ls "~1.1.2" -type-detect@^4.0.0, type-detect@^4.0.5: +type-detect@^4.0.0, type-detect@^4.0.5, type-detect@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==