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

JS keystore support #8096

Merged
merged 25 commits into from
Jun 28, 2021
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
ae62c06
Implements many building blocks but messy
eelanagaraj Jun 10, 2021
b50ac62
Cleaned up some of the impl
eelanagaraj Jun 11, 2021
b695924
Error handling for adding import key and fix tests
eelanagaraj Jun 11, 2021
466cfa4
Implements change password and some refactoring
eelanagaraj Jun 11, 2021
bf08859
Separate out IO from keystore
eelanagaraj Jun 11, 2021
44e95a2
Refactored keystore to allow for proper testing and mocking
eelanagaraj Jun 11, 2021
9d4f831
Rename Keystore to KeystoreBase and change test keys
eelanagaraj Jun 11, 2021
4a5d827
Keystore tests cleaned up
eelanagaraj Jun 14, 2021
6cf5fac
Add basic wrapper tests
eelanagaraj Jun 14, 2021
a702b54
Clean up files
eelanagaraj Jun 14, 2021
a3cab72
Small changes
eelanagaraj Jun 14, 2021
6f817e4
Small fixes for address normalization
eelanagaraj Jun 14, 2021
334ff47
Split up files a bit and rename
eelanagaraj Jun 14, 2021
f9960d5
Add easier accessor for removing keystores
eelanagaraj Jun 14, 2021
acb7cfe
Update deps
eelanagaraj Jun 14, 2021
bf8db20
Address first round of comments
eelanagaraj Jun 15, 2021
0cd334c
Add preliminary docstrings
eelanagaraj Jun 15, 2021
e89de52
Update dependency graph
eelanagaraj Jun 17, 2021
54adfd6
Remove console logs
eelanagaraj Jun 18, 2021
d39e503
Fix node version
eelanagaraj Jun 18, 2021
8f4bd9c
Address PR comments
eelanagaraj Jun 22, 2021
449f7d1
Add integration test for FileKeystore
eelanagaraj Jun 23, 2021
bde17c4
Separate out keystore from wallets in monorepo and split up files (#8…
eelanagaraj Jun 24, 2021
1de0b64
Add additional test for FileKeystore
eelanagaraj Jun 24, 2021
b248bb8
Merge branch 'master' into eelanagaraj/js-keystore
mergify[bot] Jun 28, 2021
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
7 changes: 7 additions & 0 deletions dependency-graph.json
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,13 @@
"@celo/wallet-local"
]
},
"@celo/keystores": {
"location": "packages/sdk/keystores",
"dependencies": [
"@celo/utils",
"@celo/wallet-local"
]
},
"@celo/network-utils": {
"location": "packages/sdk/network-utils",
"dependencies": [
Expand Down
2 changes: 2 additions & 0 deletions packages/sdk/keystores/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
lib/
test-keystore-dir/
17 changes: 17 additions & 0 deletions packages/sdk/keystores/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/.devchain/
/.devchain.tar.gz
/coverage/
/node_modules/
/src/
/tmp/
/.tmp/

/tslint.json
/tsconfig.*
/jest.config.*
*.tgz

/src

/lib/**/*.test.*
/lib/test-utils
5 changes: 5 additions & 0 deletions packages/sdk/keystores/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['<rootDir>/src/**/?(*.)+(spec|test).ts?(x)'],
}
31 changes: 31 additions & 0 deletions packages/sdk/keystores/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"name": "@celo/keystores",
"version": "1.2.2-dev",
"description": "keystore implementation",
"author": "Celo",
"license": "Apache-2.0",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
"keywords": [
"celo",
"blockchain",
"sdk"
],
"scripts": {
"build": "tsc -b .",
"clean": "tsc -b . --clean",
"docs": "typedoc && ts-node ../utils/scripts/linkdocs.ts keystores",
"test": "jest --runInBand",
"lint": "tslint -c tslint.json --project .",
"prepublishOnly": "yarn build"
},
"dependencies": {
"@celo/utils": "1.2.2-dev",
"@celo/wallet-local": "1.2.2-dev",
"@types/ethereumjs-util": "^5.2.0",
"ethereumjs-wallet": "^1.0.1"
},
"engines": {
"node": ">=10"
}
}
50 changes: 50 additions & 0 deletions packages/sdk/keystores/src/file-keystore.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { trimLeading0x } from '@celo/utils/lib/address'
import { mkdirSync, readdirSync, readFileSync, rmdirSync, writeFileSync } from 'fs'
import path from 'path'
import { FileKeystore } from './file-keystore'
import { ADDRESS1, GETH_GEN_KEYSTORE1, KEYSTORE_NAME1, PASSPHRASE1, PK1 } from './test-constants'

jest.setTimeout(20000)

describe('FileKeystore tests', () => {
const parentWorkdir = path.join(__dirname, 'wallet-keystore-workdir')
let testWorkdir: string

beforeAll(() => {
mkdirSync(parentWorkdir, { recursive: true })
})

beforeEach(() => {
testWorkdir = path.join(parentWorkdir, `test-${Math.random().toString(36).substring(2, 7)}`)
mkdirSync(testWorkdir)
})

afterAll(() => {
rmdirSync(parentWorkdir, { recursive: true })
})
it('initializes keystore, imports key into keystore file, and deletes', async () => {
const keystore = new FileKeystore(testWorkdir)
expect(readdirSync(testWorkdir)).toEqual(['keystore'])
const keystorePath = path.join(testWorkdir, 'keystore')
expect(readdirSync(keystorePath).length).toBe(0)
await keystore.importPrivateKey(PK1, PASSPHRASE1)
const keystoreFiles = readdirSync(keystorePath)
expect(keystoreFiles.length).toBe(1)
const keystoreName = await keystore.getKeystoreName(ADDRESS1)
expect(readFileSync(path.join(keystorePath, keystoreFiles[0])).toString()).toEqual(
keystore.getRawKeystore(keystoreName)
)
keystore.removeKeystore(keystoreName)
expect(readdirSync(keystorePath).length).toBe(0)
})

it('reads key from file in existing keystore', async () => {
const keystorePath = path.join(testWorkdir, 'keystore')
mkdirSync(keystorePath)
writeFileSync(path.join(keystorePath, KEYSTORE_NAME1), GETH_GEN_KEYSTORE1)
const keystore = new FileKeystore(testWorkdir)
expect(await keystore.getAllKeystoreNames()).toEqual([KEYSTORE_NAME1])
expect(await keystore.listKeystoreAddresses()).toEqual([ADDRESS1])
expect(trimLeading0x(await keystore.getPrivateKey(ADDRESS1, PASSPHRASE1))).toEqual(PK1)
})
})
56 changes: 56 additions & 0 deletions packages/sdk/keystores/src/file-keystore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { mkdirSync, promises as fsPromises, readFileSync, unlinkSync, writeFileSync } from 'fs'
import path from 'path'
import { KeystoreBase } from './keystore-base'

export class FileKeystore extends KeystoreBase {
/**
* Implements keystore as a directory on disk
* with files for keystore entries
*/
private _keystoreDir: string

/**
* Creates (but does not overwrite existing) directory
* for containing keystore entries.
* @param keystoreDir Path to directory where keystore will be saved
*/
constructor(keystoreDir: string) {
super()
this._keystoreDir = path.join(keystoreDir, 'keystore')
// Does not overwrite existing directories
mkdirSync(this._keystoreDir, { recursive: true })
}

/**
* @returns List of file names (keystore entries) in the keystore
*/
async getAllKeystoreNames(): Promise<string[]> {
return fsPromises.readdir(this._keystoreDir)
}

/**
* Saves keystore entries as a file in the keystore directory
* @param keystoreName File name of keystore entry
* @param keystore V3Keystore string entry
*/
persistKeystore(keystoreName: string, keystore: string) {
writeFileSync(path.join(this._keystoreDir, keystoreName), keystore)
}

/**
* Gets contents of keystore entry file
* @param keystoreName File name of keystore entry
* @returns V3Keystore string entry
*/
getRawKeystore(keystoreName: string): string {
return readFileSync(path.join(this._keystoreDir, keystoreName)).toString()
}

/**
* Deletes file keystore entry from directory
* @param keystoreName File name of keystore entry to be removed
*/
removeKeystore(keystoreName: string) {
return unlinkSync(path.join(this._keystoreDir, keystoreName))
}
}
4 changes: 4 additions & 0 deletions packages/sdk/keystores/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './file-keystore'
export * from './inmemory-keystore'
export * from './keystore-base'
export * from './keystore-wallet-wrapper'
25 changes: 25 additions & 0 deletions packages/sdk/keystores/src/inmemory-keystore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { KeystoreBase } from './keystore-base'

export class InMemoryKeystore extends KeystoreBase {
/**
* Used for mocking keystore operations in unit tests
*/
private _storage: Record<string, string> = {}

// Implements required abstract class methods
persistKeystore(keystoreName: string, keystore: string) {
this._storage[keystoreName] = keystore
}

getRawKeystore(keystoreName: string): string {
return this._storage[keystoreName]
}

async getAllKeystoreNames(): Promise<string[]> {
return Object.keys(this._storage)
}

removeKeystore(keystoreName: string) {
delete this._storage[keystoreName]
}
}
97 changes: 97 additions & 0 deletions packages/sdk/keystores/src/keystore-base.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { privateKeyToAddress, trimLeading0x } from '@celo/utils/lib/address'
import { InMemoryKeystore } from './inmemory-keystore'
import { ErrorMessages } from './keystore-base'
import {
ADDRESS1,
ADDRESS2,
GETH_GEN_KEYSTORE1,
GETH_GEN_KEYSTORE2,
KEYSTORE_NAME1,
KEYSTORE_NAME2,
PASSPHRASE1,
PASSPHRASE2,
PK1,
} from './test-constants'

jest.setTimeout(20000)

describe('KeystoreBase functionality via InMemoryKeystore (mock)', () => {
let keystore: InMemoryKeystore
beforeEach(() => {
keystore = new InMemoryKeystore()
})

describe('checks with an empty keystore', () => {
it('lists no addresses', async () => {
expect(await keystore.listKeystoreAddresses()).toEqual([])
})
it('imports keystore (can decrypt, can list addresses)', async () => {
await keystore.importPrivateKey(PK1, PASSPHRASE1)
expect(await keystore.listKeystoreAddresses()).toEqual([ADDRESS1])
expect(trimLeading0x(await keystore.getPrivateKey(ADDRESS1, PASSPHRASE1))).toEqual(PK1)
})
it('gets empty address map', async () => {
expect(await keystore.getAddressMap()).toEqual({})
})
it('fails when address is not in the keystore', async () => {
await expect(keystore.getPrivateKey(ADDRESS1, PK1)).rejects.toThrow(
ErrorMessages.NO_MATCHING_ENTRY
)
})
})

describe('checks with a populated keystore', () => {
beforeEach(() => {
keystore.persistKeystore(KEYSTORE_NAME1, GETH_GEN_KEYSTORE1)
})

it('decrypts and returns raw private key from keystore blob', async () => {
expect(trimLeading0x(await keystore.getPrivateKey(ADDRESS1, PASSPHRASE1))).toBe(PK1)
})

it('decrypts when non-normalized address is passed in', async () => {
expect(
trimLeading0x(await keystore.getPrivateKey(privateKeyToAddress(PK1), PASSPHRASE1))
).toBe(PK1)
})

it('does not decrypt keystore with incorrect passphrase', async () => {
await expect(keystore.getPrivateKey(ADDRESS1, PASSPHRASE1 + '!')).rejects.toThrow(
'Key derivation failed - possibly wrong passphrase'
)
})

it('gets keystore name from address', async () => {
expect(await keystore.getKeystoreName(ADDRESS1)).toBe(KEYSTORE_NAME1)
})

it('changes keystore passphrase successfully', async () => {
await keystore.changeKeystorePassphrase(ADDRESS1, PASSPHRASE1, PASSPHRASE2)
await expect(keystore.getPrivateKey(ADDRESS1, PASSPHRASE1)).rejects.toThrow()
expect(trimLeading0x(await keystore.getPrivateKey(ADDRESS1, PASSPHRASE2))).toBe(PK1)
})

it('does not import same private key twice', async () => {
await expect(keystore.importPrivateKey(PK1, PASSPHRASE2)).rejects.toThrow(
ErrorMessages.KEYSTORE_ENTRY_EXISTS
)
})

it('lists addresses', async () => {
expect(await keystore.listKeystoreAddresses()).toEqual([ADDRESS1])
})

it('deletes keystore', async () => {
await keystore.deleteKeystore(ADDRESS1)
expect(await keystore.listKeystoreAddresses()).toEqual([])
})

it('maps address to keystore name', async () => {
keystore.persistKeystore(KEYSTORE_NAME2, GETH_GEN_KEYSTORE2)
const expectedMap: Record<string, string> = {}
expectedMap[ADDRESS1] = KEYSTORE_NAME1
expectedMap[ADDRESS2] = KEYSTORE_NAME2
expect(await keystore.getAddressMap()).toEqual(expectedMap)
})
})
})
Loading