Skip to content

Commit

Permalink
Merge pull request #3 from aragon/f/backported-encryption
Browse files Browse the repository at this point in the history
Backported encryption
  • Loading branch information
carlosgj94 authored Jun 4, 2024
2 parents 71f49e1 + 0f653ed commit 7edaa78
Show file tree
Hide file tree
Showing 12 changed files with 554 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Pull request build
name: Pull request build and test
on:
pull_request:

Expand Down Expand Up @@ -29,3 +29,6 @@ jobs:
NEXT_PUBLIC_IPFS_API_KEY: x
NEXT_PUBLIC_ETHERSCAN_API_KEY: x
NODE_ENV: production

- name: Run tests
run: bun test
Binary file modified bun.lockb
Binary file not shown.
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"format": "prettier --check .; echo 'To write the changes: bun run format:fix'",
"format:fix": "prettier -w . --list-different",
"deploy-dao": "bun ./scripts/deploy.ts",
"prepare": "husky"
"prepare": "husky",
"test": "bun test"
},
"lint-staged": {
"*.{js, jsx,ts,tsx}": [
Expand All @@ -34,6 +35,7 @@
"ipfs-http-client": "^60.0.1",
"next": "14.1.4",
"react": "^18.2.0",
"libsodium-wrappers": "^0.7.13",
"react-blockies": "^1.4.1",
"react-dom": "^18.2.0",
"tailwindcss-fluid-type": "^2.0.6",
Expand All @@ -42,9 +44,11 @@
},
"devDependencies": {
"@aragon/osx-commons-configs": "^0.2.0",
"@types/bun": "latest",
"@types/dompurify": "^3.0.5",
"@types/node": "^20.11.30",
"@types/react": "^18.2.71",
"@types/libsodium-wrappers": "^0.7.13",
"@types/react-blockies": "^1.4.4",
"@types/react-dom": "^18.2.22",
"autoprefixer": "^10.4.19",
Expand Down
4 changes: 2 additions & 2 deletions plugins/dualGovernance/components/proposal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export default function ProposalCard(props: ProposalInputs) {
address: PUB_TOKEN_ADDRESS,
abi: erc20Votes,
functionName: "getPastTotalSupply",
args: [proposal?.parameters.snapshotBlock],
args: [proposal?.parameters.snapshotBlock || BigInt(0)],
});

const proposalVariant = useProposalStatus(proposal!);
Expand Down Expand Up @@ -86,7 +86,7 @@ export default function ProposalCard(props: ProposalInputs) {
result={{
option: "Veto",
voteAmount: proposal.vetoTally.toString(),
votePercentage: Number((proposal?.vetoTally * BigInt(100)) / pastSupply),
votePercentage: pastSupply ? Number((proposal?.vetoTally * BigInt(100)) / pastSupply) : 0,
}}
publisher={[{ address: proposal.creator }]} // Fix: Pass an object of type IPublisher instead of a string
status={proposalVariant!}
Expand Down
2 changes: 1 addition & 1 deletion plugins/multisig/hooks/useProposalExecute.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useEffect } from "react";
import { useReadContract, useWaitForTransactionReceipt, useWriteContract } from "wagmi";
import { MultisigPluginAbi } from "../artifacts/MultisigPlugin";
import { AlertContextProps, useAlerts } from "@/context/Alerts";
import { useRouter } from "next/router";
import { PUB_CHAIN, PUB_DUAL_GOVERNANCE_PLUGIN_ADDRESS } from "@/constants";
import { OptimisticTokenVotingPluginAbi } from "@/plugins/dualGovernance/artifacts/OptimisticTokenVotingPlugin.sol";

export function useProposalExecute(proposalId: string) {
const { reload } = useRouter();
Expand Down
90 changes: 90 additions & 0 deletions tests/asymmetric.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { expect, test, describe, beforeAll } from "bun:test";
import {
decryptBytes,
decryptString,
encrypt,
generateKeyPair,
getSeededKeyPair,
computePublicKey,
} from "../utils/encryption/asymmetric";
import libsodium from "libsodium-wrappers";

describe("Symmetric encryption", () => {
beforeAll(async () => {
await libsodium.ready;
});

test("Generates a random key pair", () => {
const alice = generateKeyPair();
const bob = generateKeyPair();

expect(libsodium.to_hex(alice.keyType)).toBe(libsodium.to_hex(bob.keyType));
expect(libsodium.to_hex(alice.privateKey)).not.toBe(
libsodium.to_hex(bob.privateKey)
);
expect(libsodium.to_hex(alice.publicKey)).not.toBe(
libsodium.to_hex(bob.publicKey)
);
});

test("Computes the public key given the secret one", () => {
const alice = generateKeyPair();
const computedPubKey = computePublicKey(alice.privateKey);

expect(libsodium.to_hex(alice.publicKey)).toBe(
libsodium.to_hex(computedPubKey)
);
});

test("Generates a seeded key pair", () => {
const alice = getSeededKeyPair(
"00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"
);
const bob = getSeededKeyPair(
"00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"
);

expect(libsodium.to_hex(alice.keyType)).toBe(libsodium.to_hex(bob.keyType));
expect(libsodium.to_hex(alice.privateKey)).toBe(
libsodium.to_hex(bob.privateKey)
);
expect(libsodium.to_hex(alice.publicKey)).toBe(
libsodium.to_hex(bob.publicKey)
);
});

test("Encrypts and decrypts a string", () => {
const bob = generateKeyPair();
const ciphertext = encrypt("Hello world", bob.publicKey);
expect(ciphertext.length).toBe(59);
const decrypted = decryptString(ciphertext, bob);
expect(decrypted).toBe("Hello world");
const decryptedHex = decryptBytes(ciphertext, bob);
expect(libsodium.to_hex(decryptedHex)).toBe("48656c6c6f20776f726c64");
});

test("Encrypts and decrypts a buffer", () => {
const bob = generateKeyPair();
const bytes = new Uint8Array([10, 15, 50, 55, 80, 85]);
const ciphertext = encrypt(bytes, bob.publicKey);
expect(ciphertext.length).toBe(54);
const decryptedHex = decryptBytes(ciphertext, bob);
expect(libsodium.to_hex(decryptedHex)).toBe("0a0f32375055");
});

test("Unintended keys can't decrypt", () => {
const bob = generateKeyPair();
const cindy = generateKeyPair();

const bytes = new Uint8Array([10, 15, 50, 55, 80, 85]);

const ciphertext1 = encrypt(bytes, bob.publicKey);
const ciphertext2 = encrypt("Hello world", bob.publicKey);

expect(() => decryptBytes(ciphertext1, bob)).not.toThrow();
expect(() => decryptString(ciphertext2, bob)).not.toThrow();

expect(() => decryptBytes(ciphertext1, cindy)).toThrow();
expect(() => decryptString(ciphertext2, cindy)).toThrow();
});
});
187 changes: 187 additions & 0 deletions tests/proposal-encryption.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import { expect, test, describe, beforeAll } from "bun:test";
import {
encryptProposal,
encryptSymmetricKey,
decryptProposal,
decryptSymmetricKey,
} from "../utils/encryption/index";
import libsodium from "libsodium-wrappers";
import { generateSymmetricKey } from "@/utils/encryption/symmetric";
import { generateKeyPair } from "@/utils/encryption/asymmetric";

describe("Proposal data encryption", () => {
beforeAll(async () => {
await libsodium.ready;
});

test("Encrypts a proposal data with the returned random, symmetric key", () => {
const metadata = {
title: "Proposal",
description: "Testing encryption",
};
const actionBytes = new Uint8Array([
50, 25, 40, 200, 123, 234, 55, 26, 73, 84, 62, 162, 188, 126, 255, 0, 2,
0, 5, 1, 55, 26, 37, 82,
]);

const { data: data1, symmetricKey: symmetricKey1 } = encryptProposal(
metadata,
actionBytes
);
const { data: data2, symmetricKey: symmetricKey2 } = encryptProposal(
metadata,
actionBytes
);

expect(libsodium.to_hex(data1.metadata)).not.toBe(
libsodium.to_hex(data2.metadata)
);
expect(libsodium.to_hex(data1.actions)).not.toBe(
libsodium.to_hex(data2.actions)
);
expect(libsodium.to_hex(symmetricKey1)).not.toBe(
libsodium.to_hex(symmetricKey2)
);
});

test("The returned symmetric key decrypts the original payload", () => {
const metadata1 = {
title: "Proposal 1",
description: "Testing encryption 1",
};
const actionBytes1 = new Uint8Array([
50, 25, 40, 200, 123, 234, 55, 26, 73, 84, 62, 162, 188, 126, 255, 0, 2,
0, 5, 1, 55, 26, 37, 82,
]);
const metadata2 = {
title: "Proposal 2",
description: "Testing encryption 2",
};
const actionBytes2 = new Uint8Array([
0, 5, 1, 55, 26, 37, 82, 50, 25, 40, 200, 123, 234, 55, 26, 73, 84, 62,
162, 188, 126, 255, 0, 2,
]);

// Encrypt
const { data: data1, symmetricKey: symmetricKey1 } = encryptProposal(
metadata1,
actionBytes1
);
const { data: data2, symmetricKey: symmetricKey2 } = encryptProposal(
metadata2,
actionBytes2
);

// Decrypt
const { metadata: dMetadata1, actions: dActions1 } = decryptProposal<
typeof metadata1
>(data1, symmetricKey1);
const { metadata: dMetadata2, actions: dActions2 } = decryptProposal<
typeof metadata2
>(data2, symmetricKey2);

// Check
expect(dMetadata1.title).toBe(metadata1.title);
expect(dMetadata1.description).toBe(metadata1.description);

expect(dMetadata2.title).toBe(metadata2.title);
expect(dMetadata2.description).toBe(metadata2.description);

expect(libsodium.to_hex(dActions1)).toBe(libsodium.to_hex(actionBytes1));
expect(libsodium.to_hex(dActions2)).toBe(libsodium.to_hex(actionBytes2));
});

test("Keys different than the original one can't decrypt the payload", () => {
const metadata = {
title: "Proposal title",
description: "Proposal description",
};
const actionBytes1 = new Uint8Array([
50, 73, 84, 62, 162, 188, 126, 255, 0, 2, 25, 40, 200, 123, 234, 55, 26,
55, 26, 37, 82, 0, 5, 1,
]);

// Encrypt
const { data, symmetricKey } = encryptProposal(metadata, actionBytes1);

const otherKeys = new Array(20).fill(0).map(() => generateSymmetricKey());

// Decrypt
expect(() => {
const { metadata: dMetadata, actions: dActions1 } = decryptProposal<
typeof metadata
>(data, symmetricKey);

expect(dMetadata.title).toBe(metadata.title);
expect(dMetadata.description).toBe(metadata.description);
}).not.toThrow();

for (const otherKey of otherKeys) {
expect(() => {
decryptProposal(data, otherKey);
}).toThrow();
}
});
});

describe("Symmetric key encryption across members", () => {
beforeAll(async () => {
await libsodium.ready;
});

test("Encrypts a symmetric key for N recipients", () => {
const symKey = generateSymmetricKey();

const members = new Array(13).fill(0).map(() => generateKeyPair());
const encryptedItems = encryptSymmetricKey(
symKey,
members.map((m) => m.publicKey)
);

expect(encryptedItems.length).toBe(members.length);
encryptedItems.forEach((item) => {
expect(item.length).toBe(80);
});
});

test("Recipients can decrypt the original symmetric key", () => {
const symKey = generateSymmetricKey();
const hexSymKey = libsodium.to_hex(symKey);

const members = new Array(13).fill(0).map(() => generateKeyPair());
const encryptedItems = encryptSymmetricKey(
symKey,
members.map((m) => m.publicKey)
);

for (let i = 0; i < encryptedItems.length; i++) {
const decryptedKey = decryptSymmetricKey(encryptedItems, members[i]);
expect(libsodium.to_hex(decryptedKey)).toBe(hexSymKey);
}
});

test("Non recipients cannot decrypt the original symmetric key", () => {
const symKey = generateSymmetricKey();

const members = new Array(13).fill(0).map(() => generateKeyPair());
const intruders = new Array(members.length)
.fill(0)
.map(() => generateKeyPair());
const encryptedItems = encryptSymmetricKey(
symKey,
members.map((m) => m.publicKey)
);

for (let i = 0; i < encryptedItems.length; i++) {
expect(() => {
decryptSymmetricKey(encryptedItems, intruders[i]);
}).toThrow();
}

for (let i = 0; i < encryptedItems.length; i++) {
expect(() => {
decryptSymmetricKey(encryptedItems, members[i]);
}).not.toThrow();
}
});
});
Loading

0 comments on commit 7edaa78

Please sign in to comment.