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

Consistent Encryption API #12

Merged
merged 2 commits into from
Jan 31, 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
15 changes: 7 additions & 8 deletions ts-lib/src/encryption/base58.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@
* It relies on base58 encoding and requires a nonce to decrypt the key!
*/

import { HDNodeWallet } from "ethers";
import { KeyContract } from "../keyContract";
import { EthKeyManager } from "./interface";
import { NearAccount } from "../types";
import { EthPrivateKey, NearAccount } from "../types";
import bs58 from "bs58";
import { KeyPair } from "near-api-js";
import { create, open } from "@nearfoundation/near-js-encryption-box";
Expand All @@ -21,11 +20,11 @@ export class Base58KeyManager implements EthKeyManager {
}

async encryptAndSetKey(
ethWallet: HDNodeWallet,
ethPrivateKey: EthPrivateKey,
encryptionKey: string,
): Promise<string | undefined> {
let keyPair = KeyPair.fromString(encryptionKey);
let encodedEthKey = this.encodeEthKey(ethWallet.privateKey);
let encodedEthKey = this.encodeEthKey(ethPrivateKey.toString());
const { secret: encryptedKey, nonce } = create(
encodedEthKey,
keyPair.getPublicKey().toString(),
Expand All @@ -39,21 +38,21 @@ export class Base58KeyManager implements EthKeyManager {
async retrieveAndDecryptKey(
nearAccount: NearAccount,
nonce?: string,
): Promise<string> {
): Promise<EthPrivateKey> {
const retrievedKey = await this.contract.methods.get_key({
account_id: nearAccount.accountId,
});
let keyPair = KeyPair.fromString(nearAccount.privateKey);
let keyPair = KeyPair.fromString(nearAccount.privateKey.toString());
const decryptedKey = open(
retrievedKey!,
keyPair.getPublicKey().toString(),
nearAccount.privateKey,
nearAccount.privateKey.toString(),
nonce!,
);
if (decryptedKey === null) {
throw new Error("Unable to decrypt key!");
}
return this.decodeEthKey(decryptedKey);
return new EthPrivateKey(this.decodeEthKey(decryptedKey));
}

private encodeEthKey(key: string): string {
Expand Down
16 changes: 9 additions & 7 deletions ts-lib/src/encryption/cryptojs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@
* It relies on base58 encoding and requires a nonce to decrypt the key!
*/

import { HDNodeWallet } from "ethers";
import { KeyContract } from "../keyContract";
import { EthKeyManager } from "./interface";
import { NearAccount } from "../types";
import { EthPrivateKey, NearAccount } from "../types";
import CryptoJS from "crypto-js";

export class CryptoJSKeyManager implements EthKeyManager {
Expand All @@ -19,11 +18,11 @@ export class CryptoJSKeyManager implements EthKeyManager {
}

async encryptAndSetKey(
ethWallet: HDNodeWallet,
ethPrivateKey: EthPrivateKey,
encryptionKey: string,
): Promise<string | undefined> {
let encryptedKey = CryptoJS.AES.encrypt(
ethWallet.privateKey,
ethPrivateKey.toString(),
encryptionKey,
);
console.log("Posting Encrypted Key", encryptedKey.toString());
Expand All @@ -36,11 +35,14 @@ export class CryptoJSKeyManager implements EthKeyManager {
async retrieveAndDecryptKey(
nearAccount: NearAccount,
// nonce?: string | undefined,
): Promise<string> {
): Promise<EthPrivateKey> {
const retrievedKey = await this.contract.methods.get_key({
account_id: nearAccount.accountId,
});
let bytes = CryptoJS.AES.decrypt(retrievedKey!, nearAccount.privateKey);
return bytes.toString(CryptoJS.enc.Utf8);
let bytes = CryptoJS.AES.decrypt(
retrievedKey!,
nearAccount.privateKey.toString(),
);
return new EthPrivateKey(bytes.toString(CryptoJS.enc.Utf8));
}
}
12 changes: 4 additions & 8 deletions ts-lib/src/encryption/interface.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,19 @@
import { ethers } from "ethers";
import { NearAccount } from "../types";
import { EthPrivateKey, NearAccount } from "../types";

export interface EthKeyManager {
/**
*
* @param ethWallet - Ethereum Wallet to be stored on key contract.
* @param ethPrivateKey - Ethereum Private Key to be stored on key contract.
* @param encryptionKey - Secret key of for encryption.
* @returns Nonce if needed decrypt encoded key, otherwise nothing.
*/
encryptAndSetKey(
ethWallet: ethers.HDNodeWallet,
ethPrivateKey: EthPrivateKey,
encryptionKey: string,
): Promise<string | undefined>;

retrieveAndDecryptKey(
nearAccount: NearAccount,
nonce?: string,
): Promise<string>;

// encodeEthKey(key: string): string;
// decodeEthKey(key: string): string;
): Promise<EthPrivateKey>;
}
55 changes: 54 additions & 1 deletion ts-lib/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,57 @@
// TODO - can validate whether key provided can actually control accountId
// however, it requires calls to near.
export interface NearAccount {
accountId: string;
privateKey: string;
privateKey: NearPrivateKey;
}

export class EthPrivateKey {
private key: string;

constructor(key: string) {
if (!this.isValidEthPrivateKey(key)) {
throw new Error("Invalid Ethereum private key");
}
this.key = key;
}

private isValidEthPrivateKey(key: string): boolean {
const hexRegex = /^[a-fA-F0-9]{64}$/;
return (
typeof key === "string" &&
hexRegex.test(key.startsWith("0x") ? key.slice(2) : key)
);
}

toString(): string {
return this.key;
}
}

export class NearPrivateKey {
private key: string;

constructor(key: string) {
if (!this.isNearPrivateKey(key)) {
throw new Error("Invalid Near private key");
}
this.key = key;
}

private isNearPrivateKey(key: any): boolean {
const prefix = "ed25519:";
// Base58 regex excluding 0, O, I, and l
const base58Regex = /^[A-HJ-NP-Za-km-z1-9]+$/;

if (typeof key !== "string" || !key.startsWith(prefix)) {
return false;
}

const keyPart = key.substring(prefix.length);
return base58Regex.test(keyPart);
}

toString(): string {
return this.key;
}
}
38 changes: 22 additions & 16 deletions ts-lib/tests/contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ import { connect, Contract, KeyPair, keyStores, utils } from "near-api-js";
import { ethers } from "ethers";
import * as dotenv from "dotenv";
import * as path from "path";
import { Base58KeyManager, KeyContract, CryptoJSKeyManager } from "../src";
import {
Base58KeyManager,
KeyContract,
CryptoJSKeyManager,
EthPrivateKey,
NearPrivateKey,
} from "../src";

dotenv.config({
path: path.resolve(__dirname, "../../neardev/dev-account.env"),
Expand All @@ -14,13 +20,13 @@ if (!contractName) {
throw new Error("CONTRACT_NAME not found in environment");
}

const privateKey = process.env.TEST_PK as string;
if (!privateKey) {
const nearPrivateKey = process.env.TEST_PK as string;
if (!nearPrivateKey) {
throw new Error("TEST_PK not found in environment");
}

const keyStore = new keyStores.InMemoryKeyStore();
let keyPair = KeyPair.fromString(privateKey);
let keyPair = KeyPair.fromString(nearPrivateKey);
const accountId = process.env.TEST_ACCOUNT_ID!;

async function initContract(): Promise<KeyContract> {
Expand All @@ -45,37 +51,37 @@ async function initContract(): Promise<KeyContract> {

describe("EthKeys contract tests", () => {
let contract: KeyContract;
let ethPk: EthPrivateKey;
let nearPk: NearPrivateKey;

beforeAll(async () => {
contract = await initContract();
ethPk = new EthPrivateKey(
"0x38b499b2263de8d23944746a6922757e8da6184828d98fbfd6c88ebee1fad111",
);
nearPk = new NearPrivateKey(nearPrivateKey);
});

it("Base58 RoundTrip", async () => {
const keyManager = new Base58KeyManager(contract);

// TODO - can we create not random?
const ethWallet = ethers.Wallet.createRandom();

// TODO - include nonce on contract so we don't have to remember it.
const nonce = await keyManager.encryptAndSetKey(ethWallet, privateKey);
const nonce = await keyManager.encryptAndSetKey(ethPk, nearPrivateKey);

const decryptedKey = await keyManager.retrieveAndDecryptKey(
{ accountId, privateKey },
{ accountId, privateKey: nearPk },
nonce,
);
expect(decryptedKey).toBe(ethWallet.privateKey);
expect(decryptedKey).toStrictEqual(ethPk);
});

it("CryptoJS-AES RoundTrip", async () => {
const keyManager = new CryptoJSKeyManager(contract);
const ethWallet = ethers.Wallet.createRandom();

await keyManager.encryptAndSetKey(ethWallet, privateKey);
await keyManager.encryptAndSetKey(ethPk, nearPrivateKey);

const decryptedKey = await keyManager.retrieveAndDecryptKey({
accountId,
privateKey,
privateKey: nearPk,
});
expect(decryptedKey).toBe(ethWallet.privateKey);
expect(decryptedKey).toStrictEqual(ethPk);
});
});
17 changes: 17 additions & 0 deletions ts-lib/tests/types.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { EthPrivateKey, NearPrivateKey } from "../src/types";


describe("Type Validation", () => {

it("Validates EthPrivateKey Construction", async () => {
const validKey = "0x38b499b2263de8d23944746a6922757e8da6184828d98fbfd6c88ebee1fad111";
expect(() => new EthPrivateKey(validKey)).not.toThrow();
expect(() => new EthPrivateKey('invalid key')).toThrow("Invalid Ethereum private key");
});

it("Validates NearPrivateKey Construction", async () => {
const validKey = "ed25519:TxtD94WwG6VRnJbwdhwJX4KASbSXXwovSJ3a6PK8cM63fuWcuXQ4zTTRzSmF2r8Af2bvKWKvNDyfcGRbVXbqCL1";
expect(() => new NearPrivateKey(validKey)).not.toThrow();
expect(() => new NearPrivateKey('invalid key')).toThrow("Invalid Near private key");
});
});
Loading