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

TS Library src #8

Merged
merged 3 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
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);
});
});