Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding encryption primitives #1

Merged
merged 4 commits into from
Apr 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -9,7 +9,8 @@
"lint": "next lint",
"prettier": "prettier -c ./components ./context ./hooks ./pages ./plugins ./scripts ./utils ./constants.ts ; echo 'To write the changes: npm run prettier:write'",
"prettier:write": "prettier -w ./components ./context ./hooks ./pages ./plugins ./scripts ./utils ./constants.ts",
"deploy-dao": "bun ./scripts/deploy.ts"
"deploy-dao": "bun ./scripts/deploy.ts",
"test": "bun test"
},
"dependencies": {
"@aragon/ods": "1.0.15",
Expand All @@ -22,6 +23,7 @@
"dayjs": "^1.11.10",
"dompurify": "^3.0.8",
"ipfs-http-client": "^60.0.1",
"libsodium-wrappers": "^0.7.13",
"next": "14.1.0",
"react": "^18",
"react-blockies": "^1.4.1",
Expand All @@ -32,7 +34,9 @@
},
"devDependencies": {
"@aragon/osx-commons-configs": "^0.2.0",
"@types/bun": "latest",
"@types/dompurify": "^3.0.5",
"@types/libsodium-wrappers": "^0.7.13",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-blockies": "^1.4.4",
Expand Down
70 changes: 70 additions & 0 deletions tests/asymmetric.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
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");
});
});
47 changes: 47 additions & 0 deletions tests/symmetric.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { expect, test, describe, beforeAll } from "bun:test";
import {
decryptBytes,
decryptString,
encrypt,
generateSymmetricKey,
} from "../utils/encryption/symmetric";
import libsodium from "libsodium-wrappers";

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

test("Generates a random symmetric key", () => {
const key1 = generateSymmetricKey();
const key2 = generateSymmetricKey();

expect(libsodium.to_hex(key1)).not.toBe(libsodium.to_hex(key2));
});

test("Encrypts and decrypts a string", () => {
const symKey = generateSymmetricKey();
const encryptedPayload = encrypt("Hello world", symKey);

expect(libsodium.to_hex(encryptedPayload)).toMatch(/^[0-9a-fA-F]+$/);
expect(encryptedPayload.length).toBe(51);

const decrypted = decryptString(encryptedPayload, symKey);
expect(decrypted).toBe("Hello world");

const decryptedHex = decryptBytes(encryptedPayload, symKey);
expect(libsodium.to_hex(decryptedHex)).toBe("48656c6c6f20776f726c64");
});

test("Encrypts and decrypts a buffer", () => {
const symKey = generateSymmetricKey();
const bytes = new Uint8Array([10, 15, 50, 55, 80, 85]);
const encryptedPayload = encrypt(bytes, symKey);

expect(libsodium.to_hex(encryptedPayload)).toMatch(/^[0-9a-fA-F]+$/);
expect(encryptedPayload.length).toBe(46);

const decryptedHex = decryptBytes(encryptedPayload, symKey);
expect(libsodium.to_hex(decryptedHex)).toBe("0a0f32375055");
});
});
46 changes: 46 additions & 0 deletions utils/encryption/asymmetric.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import sodium, { KeyPair } from "libsodium-wrappers";
export type { KeyPair } from "libsodium-wrappers";

export function encrypt(
message: string | Uint8Array,
recipientPubKey: Uint8Array
) {
return sodium.crypto_box_seal(message, recipientPubKey, "uint8array");
}

export function decryptString(ciphertext: Uint8Array, keyPair: KeyPair) {
const bytes = decryptBytes(ciphertext, keyPair);
return sodium.to_string(bytes);
}

export function decryptBytes(
ciphertext: Uint8Array,
keyPair: KeyPair
): Uint8Array {
return sodium.crypto_box_seal_open(
ciphertext,
keyPair.publicKey,
keyPair.privateKey,
"uint8array"
);
}

// Key management

export function generateKeyPair() {
return sodium.crypto_box_keypair();
}

export function getSeededKeyPair(hexSeed: string) {
if (!hexSeed.match(/^[0-9a-fA-F]+$/)) {
throw new Error("Invalid hexadecimal seed");
} else if (hexSeed.length != 64) {
throw new Error("The hexadecimal seed should be 32 bytes long");
}
const bytesSeed = sodium.from_hex(hexSeed);
return sodium.crypto_box_seed_keypair(bytesSeed);
}

export function computePublicKey(secretKey: Uint8Array) {
return sodium.crypto_scalarmult_base(secretKey);
}
52 changes: 52 additions & 0 deletions utils/encryption/symmetric.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import sodium from "libsodium-wrappers";
import { concatenate } from "./util";

const SYM_KEY_LENGTH = 32;

export function encrypt(
message: string | Uint8Array,
symmetricKey: Uint8Array
) {
const nonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES);
return concatenate([
nonce,
sodium.crypto_secretbox_easy(message, nonce, symmetricKey),
]);
}

export function decryptString(
xavikh marked this conversation as resolved.
Show resolved Hide resolved
nonceAndCiphertext: Uint8Array,
symmetricKey: Uint8Array
) {
const bytes = decryptBytes(nonceAndCiphertext, symmetricKey);
return sodium.to_string(bytes);
}

export function decryptBytes(
nonceAndCiphertext: Uint8Array,
symmetricKey: Uint8Array
) {
const minLength =
sodium.crypto_secretbox_NONCEBYTES + sodium.crypto_secretbox_MACBYTES;
if (nonceAndCiphertext.length < minLength) {
throw "Invalid encrypted payload";
}

const nonce = nonceAndCiphertext.slice(0, sodium.crypto_secretbox_NONCEBYTES);
const ciphertext = nonceAndCiphertext.slice(
sodium.crypto_secretbox_NONCEBYTES
);

return sodium.crypto_secretbox_open_easy(
ciphertext,
nonce,
symmetricKey,
"uint8array"
);
}

// Key helpers

export function generateSymmetricKey(size: number = SYM_KEY_LENGTH) {
return sodium.randombytes_buf(size, "uint8array");
}
16 changes: 16 additions & 0 deletions utils/encryption/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export function concatenate(arrays: Uint8Array[]) {
const totalLength = arrays.reduce(
(prev, uint8array) => prev + uint8array.byteLength,
0
);

const result = new Uint8Array(totalLength);

let offset = 0;
arrays.forEach((entry) => {
result.set(entry, offset);
offset += entry.byteLength;
});

return result;
}
Loading