diff --git a/ts-lib/package.json b/ts-lib/package.json index 76d4fc1..9849e73 100644 --- a/ts-lib/package.json +++ b/ts-lib/package.json @@ -2,7 +2,7 @@ "name": "neareth", "version": "1.0.0", "description": "The smart contract exposes two methods to enable storing and retrieving an encrypted (key, value)", - "main": "index.js", + "main": "index.ts", "scripts": { "test": "jest", "lint": "eslint && prettier --check .", diff --git a/ts-lib/src/encryption/base58.ts b/ts-lib/src/encryption/base58.ts new file mode 100644 index 0000000..890b1d7 --- /dev/null +++ b/ts-lib/src/encryption/base58.ts @@ -0,0 +1,69 @@ +/** + * This Key Manager is based on the cryptography package provided by + * @nearfoundation/near-js-encryption-box + * 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 bs58 from "bs58"; +import { KeyPair } from "near-api-js"; +import { create, open } from "@nearfoundation/near-js-encryption-box"; + +export class Base58KeyManager implements EthKeyManager { + // EthKeyContract connected to account for `nearPrivateKey`. + contract: KeyContract; + + constructor(contract: KeyContract) { + this.contract = contract; + } + + async encryptAndSetKey( + ethWallet: HDNodeWallet, + encryptionKey: string, + ): Promise { + let keyPair = KeyPair.fromString(encryptionKey); + let encodedEthKey = this.encodeEthKey(ethWallet.privateKey); + const { secret: encryptedKey, nonce } = create( + encodedEthKey, + keyPair.getPublicKey().toString(), + encryptionKey, + ); + console.log("Posting Encrypted Key", encryptedKey, nonce); + await this.contract.methods.set_key({ encrypted_key: encryptedKey }); + return nonce || undefined; + } + + async retrieveAndDecryptKey( + nearAccount: NearAccount, + nonce?: string, + ): Promise { + const retrievedKey = await this.contract.methods.get_key({ + account_id: nearAccount.accountId, + }); + let keyPair = KeyPair.fromString(nearAccount.privateKey); + const decryptedKey = open( + retrievedKey!, + keyPair.getPublicKey().toString(), + nearAccount.privateKey, + nonce!, + ); + if (decryptedKey === null) { + throw new Error("Unable to decrypt key!"); + } + return this.decodeEthKey(decryptedKey); + } + + private encodeEthKey(key: string): string { + const bytes = Buffer.from(key.slice(2), "hex"); + const encodedKey = bs58.encode(bytes); + return encodedKey; + } + + private decodeEthKey(key: string): string { + const bytes = Buffer.from(bs58.decode(key)); + return "0x" + bytes.toString("hex"); + } +} diff --git a/ts-lib/src/encryption/interface.ts b/ts-lib/src/encryption/interface.ts new file mode 100644 index 0000000..af5770e --- /dev/null +++ b/ts-lib/src/encryption/interface.ts @@ -0,0 +1,23 @@ +import { ethers } from "ethers"; +import { NearAccount } from "../types"; + +export interface EthKeyManager { + /** + * + * @param ethWallet - Ethereum Wallet 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, + encryptionKey: string, + ): Promise; + + retrieveAndDecryptKey( + nearAccount: NearAccount, + nonce?: string, + ): Promise; + + // encodeEthKey(key: string): string; + // decodeEthKey(key: string): string; +} diff --git a/ts-lib/src/index.ts b/ts-lib/src/index.ts new file mode 100644 index 0000000..0d704ee --- /dev/null +++ b/ts-lib/src/index.ts @@ -0,0 +1,3 @@ +export * from "./encryption/base58"; +export * from "./keyContract"; +export * from "./types"; diff --git a/ts-lib/src/keyContract.ts b/ts-lib/src/keyContract.ts new file mode 100644 index 0000000..09dc702 --- /dev/null +++ b/ts-lib/src/keyContract.ts @@ -0,0 +1,27 @@ +import { Account, Contract } from "near-api-js"; + +export interface IKeyContract { + set_key: (args: { encrypted_key: string }) => Promise; + get_key: (args: { account_id: string }) => Promise; +} + +export class KeyContract { + // Contract method interface + methods: IKeyContract; + // Connected Account + account: Account; + + /** + * Constructs an instance of a connected KeyContract + * @param contractId - Account ID of deployed contract. + * @param account - Near Account to sign change method transactions. + */ + constructor(contractId: string, account: Account) { + this.account = account; + this.methods = new Contract(account, contractId, { + viewMethods: ["get_key"], + changeMethods: ["set_key"], + useLocalViewExecution: false, + }) as unknown as IKeyContract; + } +} diff --git a/ts-lib/src/types.ts b/ts-lib/src/types.ts new file mode 100644 index 0000000..d84cafc --- /dev/null +++ b/ts-lib/src/types.ts @@ -0,0 +1,4 @@ +export interface NearAccount { + accountId: string; + privateKey: string; +} diff --git a/ts-lib/tests/contract.test.ts b/ts-lib/tests/contract.test.ts index c86c435..9c16a14 100644 --- a/ts-lib/tests/contract.test.ts +++ b/ts-lib/tests/contract.test.ts @@ -2,20 +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"; -// @ts-ignore -import { create, open } from "@nearfoundation/near-js-encryption-box"; -import bs58 from "bs58"; +import { Base58KeyManager, KeyContract } from "../src"; dotenv.config({ path: path.resolve(__dirname, "../../neardev/dev-account.env"), }); jest.setTimeout(30000); -interface EthKeysContract { - set_key: (args: { encrypted_key: string }) => Promise; - get_key: (args: { account_id: string }) => Promise; -} - const contractName = process.env.CONTRACT_NAME as string; if (!contractName) { throw new Error("CONTRACT_NAME not found in environment"); @@ -30,7 +23,7 @@ const keyStore = new keyStores.InMemoryKeyStore(); let keyPair = KeyPair.fromString(privateKey); const accountId = process.env.TEST_ACCOUNT_ID!; -async function initContract(): Promise { +async function initContract(): Promise { await keyStore.setKey("testnet", accountId, keyPair); const config = { @@ -45,51 +38,31 @@ async function initContract(): Promise { const near = await connect(config); const account = await near.account(accountId); - const contract = new Contract(account, contractName, { - viewMethods: ["get_key"], - changeMethods: ["set_key"], - useLocalViewExecution: false, - }) as unknown as EthKeysContract; + const contract = new KeyContract(contractName, account); return contract; } -function encodeEthKey(key: string): string { - const bytes = Buffer.from(key.slice(2), "hex"); - const encodedKey = bs58.encode(bytes); - return encodedKey; -} - -function decodeEthKey(key: string): string { - const bytes = Buffer.from(bs58.decode(key)); - return "0x" + bytes.toString("hex"); -} - describe("EthKeys contract tests", () => { - let contract: EthKeysContract; + let contract: KeyContract; beforeAll(async () => { contract = await initContract(); }); - it("should generate ethWallet, encode and encrypt private key, set value on contract, retrieve, decrypt and match", async () => { + it("Base58 RoundTrip", async () => { + const keyManager = new Base58KeyManager(contract); + // TODO - can we create not random? - let ethWallet = ethers.Wallet.createRandom(); - let encodedEthKey = encodeEthKey(ethWallet.privateKey); - const { secret: encryptedKey, nonce } = create( - encodedEthKey, - keyPair.getPublicKey().toString(), - privateKey, - ); - console.log("Encrypted Key", encryptedKey, nonce); - await contract.set_key({ encrypted_key: encryptedKey }); - const retrievedKey = await contract.get_key({ account_id: accountId }); - const decryptedKey = open( - retrievedKey!, - keyPair.getPublicKey().toString(), - privateKey, + 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 decryptedKey = await keyManager.retrieveAndDecryptKey( + { accountId, privateKey }, nonce, ); - expect(decodeEthKey(decryptedKey!)).toBe(ethWallet.privateKey); + expect(decryptedKey).toBe(ethWallet.privateKey); }); });