Skip to content

Commit

Permalink
TS Library src (#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
bh2smith authored Jan 31, 2024
1 parent 1434079 commit 7990048
Show file tree
Hide file tree
Showing 7 changed files with 142 additions and 43 deletions.
2 changes: 1 addition & 1 deletion ts-lib/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 .",
Expand Down
69 changes: 69 additions & 0 deletions ts-lib/src/encryption/base58.ts
Original file line number Diff line number Diff line change
@@ -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<string | undefined> {
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<string> {
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");
}
}
23 changes: 23 additions & 0 deletions ts-lib/src/encryption/interface.ts
Original file line number Diff line number Diff line change
@@ -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<string | undefined>;

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

// encodeEthKey(key: string): string;
// decodeEthKey(key: string): string;
}
3 changes: 3 additions & 0 deletions ts-lib/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./encryption/base58";
export * from "./keyContract";
export * from "./types";
27 changes: 27 additions & 0 deletions ts-lib/src/keyContract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Account, Contract } from "near-api-js";

export interface IKeyContract {
set_key: (args: { encrypted_key: string }) => Promise<void>;
get_key: (args: { account_id: string }) => Promise<string | null>;
}

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;
}
}
4 changes: 4 additions & 0 deletions ts-lib/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface NearAccount {
accountId: string;
privateKey: string;
}
57 changes: 15 additions & 42 deletions ts-lib/tests/contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>;
get_key: (args: { account_id: string }) => Promise<string | null>;
}

const contractName = process.env.CONTRACT_NAME as string;
if (!contractName) {
throw new Error("CONTRACT_NAME not found in environment");
Expand All @@ -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<EthKeysContract> {
async function initContract(): Promise<KeyContract> {
await keyStore.setKey("testnet", accountId, keyPair);

const config = {
Expand All @@ -45,51 +38,31 @@ async function initContract(): Promise<EthKeysContract> {
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);
});
});

0 comments on commit 7990048

Please sign in to comment.