Skip to content

Commit

Permalink
feat(OCLT-28): add RSA-OAEP wrapper
Browse files Browse the repository at this point in the history
  • Loading branch information
ythepaut committed Apr 27, 2024
1 parent cf8480c commit 38d652c
Show file tree
Hide file tree
Showing 8 changed files with 153 additions and 6 deletions.
3 changes: 2 additions & 1 deletion build/exports.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export { sha256, sha512 } from "@occult-app/crypto/sha2";
export { kdf } from "@occult-app/crypto/kdf";
export { ppf } from "@occult-app/crypto/ppf";
export { hmac } from "@occult-app/crypto/hmac";
export { generateKeyPair, sign, verify } from "@occult-app/crypto/ed25519";
export { generateEd25519KeyPair, sign, verify } from "@occult-app/crypto/ed25519";
export { generateRSAKeyPair, exportAsPem, rsaEncrypt, rsaDecrypt } from "@occult-app/crypto/rsa";
1 change: 1 addition & 0 deletions build/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/ed25519.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ interface KeyPair {
*
* @returns A Ed25519 key pair.
*/
function generateKeyPair(): KeyPair {
function generateEd25519KeyPair(): KeyPair {
const secret: ByteArray = ed25519.utils.randomPrivateKey();
return {
pub: ed25519.getPublicKey(secret),
Expand Down Expand Up @@ -42,4 +42,4 @@ function verify(pub: ByteArray, signature: ByteArray, data: ByteArray): boolean
return ed25519.verify(signature, data, pub);
}

export { generateKeyPair, sign, verify };
export { generateEd25519KeyPair, sign, verify };
5 changes: 5 additions & 0 deletions src/exceptions/DecryptionException.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default class DecryptionException extends Error {
constructor(error: unknown) {
super(`Failed to decrypt data.\nDetails:\n\t:${error}`);
}
}
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ import { sha256, sha512 } from "./sha2";
import { kdf } from "./kdf";
import { ppf } from "./ppf";
import { hmac } from "./hmac";
import { generateKeyPair, sign, verify } from "./ed25519";
import { generateEd25519KeyPair, sign, verify } from "./ed25519";
import { generateRSAKeyPair, exportAsPem, rsaEncrypt, rsaDecrypt } from "./rsa";
93 changes: 93 additions & 0 deletions src/rsa.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { subtle } from "crypto";
import { ByteArray, bytesToBase64 } from "./types";
import DecryptionException from "./exceptions/DecryptionException";

interface KeyPair {
pub: ByteArray;
secret: ByteArray;
}

/**
* Generates a pair of RSA keys.
*
* @returns An RSA key pair.
*/
async function generateRSAKeyPair(): Promise<KeyPair> {
const cryptoKeyPair = await subtle.generateKey(
{
name: "RSA-OAEP",
modulusLength: 2048,
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
hash: { name: "SHA-256" }
},
true,
["encrypt", "decrypt"]
);

const pub: ByteArray = new Uint8Array(await subtle.exportKey("spki", cryptoKeyPair.publicKey));
const secret: ByteArray = new Uint8Array(
await subtle.exportKey("pkcs8", cryptoKeyPair.privateKey)
);

return { pub, secret };
}

/**
* Exports the given RSA key as PEM format.
*
* @param key The key to be exported.
* @param keyType The type of the key ("PUBLIC" for public key, "SECRET" for private key).
* @returns The RSA key in PEM format.
*/
function exportAsPem(key: ByteArray, keyType: "PUBLIC" | "SECRET"): string {
const base64Key = bytesToBase64(new Uint8Array(key));
return `-----BEGIN ${keyType == "PUBLIC" ? "PUBLIC" : "RSA PRIVATE"} KEY-----\n${base64Key}\n-----END ${keyType == "PUBLIC" ? "PUBLIC" : "RSA PRIVATE"} KEY-----`;
}

/**
* Encrypts the given data using the provided RSA public key.
*
* @param pub The RSA public key used for encryption.
* @param data The data to be encrypted.
* @returns The encrypted data.
*/
async function rsaEncrypt(pub: ByteArray, data: ByteArray): Promise<ByteArray> {
const publicKey = await subtle.importKey(
"spki",
pub,
{ name: "RSA-OAEP", hash: "SHA-256" },
true,
["encrypt"]
);

const encryptedData = await subtle.encrypt({ name: "RSA-OAEP" }, publicKey, data);

return new Uint8Array(encryptedData);
}

/**
* Decrypts the given cipher using the provided RSA private key.
*
* @param secret The RSA private key used for decryption.
* @param cipher The ciphered data to be decrypted.
* @returns The decrypted data.
*/
async function rsaDecrypt(secret: ByteArray, cipher: ByteArray): Promise<ByteArray> {
try {
const privateKey = await subtle.importKey(
"pkcs8",
secret,
{ name: "RSA-OAEP", hash: "SHA-256" },
true,
["decrypt"]
);

const decryptedData = await subtle.decrypt({ name: "RSA-OAEP" }, privateKey, cipher);

return new Uint8Array(decryptedData);
} catch (error) {
throw new DecryptionException(error);
}
}

export { generateRSAKeyPair, exportAsPem, rsaEncrypt, rsaDecrypt };
4 changes: 2 additions & 2 deletions tests/ed25519.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ByteArray, hexToBytes } from "../src/types";
import { generateKeyPair, sign, verify } from "../src/ed25519";
import { generateEd25519KeyPair, sign, verify } from "../src/ed25519";

describe("Ed25519", () => {
const secret: ByteArray = hexToBytes(
Expand All @@ -14,7 +14,7 @@ describe("Ed25519", () => {
);

it("should generate a key pair", () => {
const keyPair = generateKeyPair();
const keyPair = generateEd25519KeyPair();

expect(keyPair.secret.length).toBe(32);
expect(keyPair.pub.length).toBe(32);
Expand Down
46 changes: 46 additions & 0 deletions tests/rsa.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { exportAsPem, generateRSAKeyPair, rsaDecrypt, rsaEncrypt } from "../src/rsa";
import { base64ToBytes, ByteArray } from "../src/types";
import DecryptionException from "../src/exceptions/DecryptionException";

describe("RSA", () => {
const pub: ByteArray = base64ToBytes(
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAi/7MWgu2nObTHZ6+k//1YsA6CkS7o9FjA1yegUPb6ZTZcTFeeL/L4b/oLZZuWC76pqsobwwxyqr8uWfd5uBerCHqsXuvda30RVgLwDSXDNzXHXhAUbWafeCypMABG6MMLbjcFSmy+15fygZOHYCpmEAM1nePWUADbQTch3YM7ZvzAZEb69IKviP49Lty30KKgUZInYu4nYsqGfhINCR6E5d6su/tVWrVCHn7eoomTh2+TR9DDpRMfKuA4PstRu4wNsTIm2QUESkTlBexEPG+nCQZETe1mXfcu/zIJ+JhdZvIDp8sgDc0YP2KllpNQQAfSvZWi59WnfSrHPrn9YQXhQIDAQAB"
);
const secret: ByteArray = base64ToBytes(
"MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCL/sxaC7ac5tMdnr6T//ViwDoKRLuj0WMDXJ6BQ9vplNlxMV54v8vhv+gtlm5YLvqmqyhvDDHKqvy5Z93m4F6sIeqxe691rfRFWAvANJcM3NcdeEBRtZp94LKkwAEbowwtuNwVKbL7Xl/KBk4dgKmYQAzWd49ZQANtBNyHdgztm/MBkRvr0gq+I/j0u3LfQoqBRkidi7idiyoZ+Eg0JHoTl3qy7+1VatUIeft6iiZOHb5NH0MOlEx8q4Dg+y1G7jA2xMibZBQRKROUF7EQ8b6cJBkRN7WZd9y7/Mgn4mF1m8gOnyyANzRg/YqWWk1BAB9K9laLn1ad9Ksc+uf1hBeFAgMBAAECggEAFKo17AzSlfKUs0idhSdBR6RsbNucfyAHZ5WiaB5mNeKc/lJ1SYm2RMqZMTGFdErrAPM+8rVwHCSFTbEBVnVERFB1XmGNkudsqVfH1EVvDp0Cu/zmMamG9pHWjO8yuqukvpMNGaqgGu7hYObxXPWDE1BRFM9tP5VtbAhNrUlFu0UUAUIKXSZ9ISjR1P5akKWPqPTByny9HBRREKR62HCR/dJi+o/QmJJzgRneAjMceNDMrLPdAx9VmZq3ydFvZGoHtmzGaCdDuZkcAmhEWbZd5/kDBFhUNBxVf4Mw82168BQOquRGde7CICjqfHq9s2M6FMZPzbOp8Lf4U2djd5UVwQKBgQDBHrMMsDcmuOto9iwzhPzbhs752WUSkRMRS/LeMYZRlinhlLrk3U3vozILrrBgdt7wy4tTgf8hnpLIie7yF4dALF54G4AXLq5U6ZhKgtOxZ9PG6oAt1Lt8JbEGHLox7fnj61MkOl8OTGRNWEDzRcC4jb45LcPGjstMPmEvqMYWqQKBgQC5k/RZ4Qb09z//q/224nIh+LVejtdS/LZppju6pOcGZUi9qhFiOGp7jobIXXi+Kya8NnZpMs/lPvEKyJfehE9G04DO3Fwhsqpw4OXKiB4q4vFz1Cuv39HE/ZaHfeNEZOMTUntCRgYWIClkDYizojPgrdxGHmoMGy2TtKNh2NIvfQKBgFVIx22dDF4PX/bUcCS4YCpaWGFCPj56zt7emXzChjzVgd1bH0Ye7WIcweci2EVheYcBfJ/+Urt4Gf5x25ISDeqrLXUBQAn81YQuPND7AZ3g6kd4G+8heUKUKp82ZIvUOn0K2YawOHZKCimWBejpLvjRr+X0bfbr7J/ZFHZFpGUxAoGBALNHxUtDQqZLaq+EJ7KTpzE8hXMZSdnpPAxqxM82Wd3q4VAhyXN7DxvYgEt3SZtmMqCneNN+fCt7GACT82vLpI7G62xspDEF2vT7v+Nehs5lul9s/NuGva+yUMcKjhFX6auUBnHrFhwv6+y1ynfH3phTPjcbUNXrN6e3XhO8FKo9AoGAYkG2zNdPJdp8NEr/KX0RNhBna1BdG9010dx3d0Is59hMTWvHObmqbWCTISeccfFuY7tV0XzRaFa5aOl78Fs9+SvDya2OXWaR1SW78xC255+diBMRNee887hCKKKoi5C5uBk7n6PRamrvSnahGWlrPRO6Cl+wYMFcezNrSakCwFA="
);
const data: ByteArray = base64ToBytes("c2VjdXJpdHk=");

it("should generate a key pair", async () => {
const keyPair = await generateRSAKeyPair();

expect(keyPair.secret.length).toBeGreaterThan(1000);
expect(keyPair.pub.length).toBeGreaterThan(200);
});

it("should export keys", () => {
const publicPem = exportAsPem(pub, "PUBLIC");
const secretPem = exportAsPem(secret, "SECRET");

expect(publicPem).toMatch(/^-----BEGIN PUBLIC KEY-----[\s\w/+=]+-----END PUBLIC KEY-----$/);
expect(secretPem).toMatch(
/^-----BEGIN RSA PRIVATE KEY-----[\s\w/+=]+-----END RSA PRIVATE KEY-----$/
);
});

it("should encrypt and decrypt data", async () => {
const cipher: ByteArray = await rsaEncrypt(pub, data);
const decryptedData: ByteArray = await rsaDecrypt(secret, cipher);

expect(decryptedData).toEqual(data);
});

it("should not be able to decrypt data", async () => {
const cipher: ByteArray = await rsaEncrypt(pub, data);
const wrongKeyPair = await generateRSAKeyPair();

await expect(async () => await rsaDecrypt(wrongKeyPair.secret, cipher)).rejects.toThrow(
DecryptionException
);
});
});

0 comments on commit 38d652c

Please sign in to comment.