Skip to content

Commit

Permalink
fix(seedutil): separate aezeed & encryption pwords
Browse files Browse the repository at this point in the history
This modifies the seedutil tool so that it uses separate passwords for
the aezeed and for encrypting the private key in the keystore.
Previously, a single password was used for both, which is incompatible
with how xud uses them separately with a master password for decryption
and currently always using the default "aezeed" password for the aezeed
itself.

The encryption password, keystore path, and aezeed passwords are now
treated as optional flags.
  • Loading branch information
sangaman committed Sep 25, 2019
1 parent f32fe6d commit 977e3d9
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 41 deletions.
12 changes: 10 additions & 2 deletions seedutil/README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
# seedutil

This utility is used to derive an Ethereum keystore file from [aezeed](https://github.com/lightningnetwork/lnd/tree/master/aezeed) generated mnemonic seed.

## Build

`npm run compile:seedutil`

## Usage

It is recommended to use this tool on the command line ONLY for development purposes.
`seedutil [twenty four recovery words separated by space] [optional password] [optional keystore path]`

[Tests](/test/jest/SeedUtil.spec.ts)
`seedutil [-pass=encryption password] [-path=optional/keystore/path] [-aezeedpass=optional_seed_pass] <twenty four recovery words separated by spaces>`

By default the `keystore` folder will be created in the execution directory and the aezeed password will be `aezeed`.

## Tests

`npm run test:seedutil`
67 changes: 55 additions & 12 deletions seedutil/SeedUtil.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { exec } from 'child_process';
import { promises as fs, existsSync } from 'fs';
import { promises as fs, existsSync, lstatSync } from 'fs';

const executeCommand = (cmd: string): Promise<string> => {
return new Promise((resolve, reject) => {
Expand All @@ -25,7 +25,11 @@ const deleteDir = async (path: string) => {
const files = await fs.readdir(path);
const deletePromises: Promise<void>[] = [];
files.forEach((file) => {
deletePromises.push(fs.unlink(`${path}/${file}`));
if (lstatSync(`${path}/${file}`).isDirectory()) { // recurse
deletePromises.push(deleteDir(`${path}/${file}`));
} else { // delete file
deletePromises.push(fs.unlink(`${path}/${file}`));
}
});
await Promise.all(deletePromises);
// delete directory
Expand All @@ -38,11 +42,14 @@ const deleteDir = async (path: string) => {

const SUCCESS_KEYSTORE_CREATED = 'Keystore created';
const ERRORS = {
INVALID_MNEMONIC_LENGTH: 'expecting 24-word mnemonic seed separated by a space',
INVALID_SEED_OR_PASSWORD: 'invalid seed or password',
INVALID_ARGS_LENGTH: 'expecting password and 24-word mnemonic seed separated by spaces',
MISSING_ENCRYPTION_PASSWORD: 'expecting encryption password',
INVALID_AEZEED: 'invalid aezeed',
KEYSTORE_FILE_ALREADY_EXISTS: 'account already exists',
};

const PASSWORD = 'wasspord';

const VALID_SEED = {
seedPassword: 'mysecretpassword',
seedWords: [
Expand All @@ -54,6 +61,16 @@ const VALID_SEED = {
ethAddress: '23ccdcd149bd433d64987ffebbc88ac909842303',
};

const VALID_SEED_NO_PASS = {
seedWords: [
'abstract', 'swear', 'air', 'swamp', 'carpet', 'that',
'retire', 'pool', 'produce', 'food', 'join', 'inform',
'giraffe', 'local', 'region', 'anchor', 'march', 'advice',
'blanket', 'quick', 'farm', 'mandate', 'shell', 'lens',
],
ethAddress: 'e650ced4be22e305bd133a0b7f8e50b9c5568c57',
};

const DEFAULT_KEYSTORE_PATH = `${process.cwd()}/seedutil/keystore`;

describe('SeedUtil', () => {
Expand All @@ -63,23 +80,23 @@ describe('SeedUtil', () => {

test('it errors with no arguments', async () => {
await expect(executeCommand('./seedutil/seedutil'))
.rejects.toThrow(ERRORS.INVALID_MNEMONIC_LENGTH);
.rejects.toThrow(ERRORS.INVALID_ARGS_LENGTH);
});

test('it errors with 23 words', async () => {
const cmd = `./seedutil/seedutil ${VALID_SEED.seedWords.slice(0, 22).join(' ')}`;
const cmd = `./seedutil/seedutil ${VALID_SEED.seedWords.slice(0, 23).join(' ')}`;
await expect(executeCommand(cmd))
.rejects.toThrow(ERRORS.INVALID_MNEMONIC_LENGTH);
.rejects.toThrow(ERRORS.INVALID_ARGS_LENGTH);
});

test('it errors with 24 words and invalid password', async () => {
test('it errors with 24 words and invalid aezeed password', async () => {
const cmd = `./seedutil/seedutil ${VALID_SEED.seedWords.join(' ')}`;
await expect(executeCommand(cmd))
.rejects.toThrow(ERRORS.INVALID_SEED_OR_PASSWORD);
.rejects.toThrow(ERRORS.INVALID_AEZEED);
});

test('it succeeds with 24 words, valid password', async () => {
const cmd = `./seedutil/seedutil ${VALID_SEED.seedWords.join(' ')} ${VALID_SEED.seedPassword}`;
test('it succeeds with 24 words, valid aezeed password', async () => {
const cmd = `./seedutil/seedutil -aezeedpass=${VALID_SEED.seedPassword} ${VALID_SEED.seedWords.join(' ')}`;
await expect(executeCommand(cmd))
.resolves.toMatch(SUCCESS_KEYSTORE_CREATED);
// Read our keystore file
Expand All @@ -95,9 +112,35 @@ describe('SeedUtil', () => {
.rejects.toThrow(ERRORS.KEYSTORE_FILE_ALREADY_EXISTS);
});

test('it succeeds with 24 words, no aezeed password', async () => {
const cmd = `./seedutil/seedutil ${VALID_SEED_NO_PASS.seedWords.join(' ')}`;
await expect(executeCommand(cmd))
.resolves.toMatch(SUCCESS_KEYSTORE_CREATED);
// Read our keystore file
const files = await fs.readdir(DEFAULT_KEYSTORE_PATH);
expect(files.length).toEqual(1);
const keyStorePath = `${DEFAULT_KEYSTORE_PATH}/${files[0]}`;
const keyStoreObj = JSON.parse(await fs.readFile(keyStorePath, 'utf8'));
// verify that the derived ETH address matches
expect(keyStoreObj.address).toEqual(VALID_SEED_NO_PASS.ethAddress);
});

test('it succeeds with 24 word and encryption password', async () => {
const cmd = `./seedutil/seedutil -pass=${PASSWORD} ${VALID_SEED_NO_PASS.seedWords.join(' ')}`;
await expect(executeCommand(cmd))
.resolves.toMatch(SUCCESS_KEYSTORE_CREATED);
// Read our keystore file
const files = await fs.readdir(DEFAULT_KEYSTORE_PATH);
expect(files.length).toEqual(1);
const keyStorePath = `${DEFAULT_KEYSTORE_PATH}/${files[0]}`;
const keyStoreObj = JSON.parse(await fs.readFile(keyStorePath, 'utf8'));
// verify that the derived ETH address matches
expect(keyStoreObj.address).toEqual(VALID_SEED_NO_PASS.ethAddress);
});

test('it allows custom keystore save path', async () => {
const CUSTOM_PATH = `${process.cwd()}/seedutil/custom`;
const cmd = `./seedutil/seedutil ${VALID_SEED.seedWords.join(' ')} ${VALID_SEED.seedPassword} ${CUSTOM_PATH}`;
const cmd = `./seedutil/seedutil -path=${CUSTOM_PATH} -aezeedpass=${VALID_SEED.seedPassword} ${VALID_SEED.seedWords.join(' ')}`;
await expect(executeCommand(cmd))
.resolves.toMatch(SUCCESS_KEYSTORE_CREATED);
// cleanup custom path
Expand Down
52 changes: 25 additions & 27 deletions seedutil/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,46 +3,49 @@ package main
import (
"crypto/hmac"
"crypto/sha512"
"flag"
"fmt"
"os"
"path/filepath"

"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/crypto"
"github.com/lightningnetwork/lnd/aezeed"
"os"
"path/filepath"
)

var (
// defaultPassphrase is the default passphrase that will
// be used for decryption
defaultPassphrase = []byte("aezeed")
// be used for the seed
defaultAezeedPassphrase = "aezeed"
// masterKey is the master key used along with a random seed used to generate
// the master node in the hierarchical tree.
masterKey = []byte("Bitcoin seed")
// by default we will generate the keystore file into the keystore directory
// relative to the execution directory
defaultKeyStorePath = filepath.Join(filepath.Dir(os.Args[0]), "keystore")
defaultKeyStorePath = filepath.Join(filepath.Dir(os.Args[0]))
)

func main() {
if len(os.Args[1:]) < aezeed.NummnemonicWords {
fmt.Fprintf(os.Stderr, "\nerror: expecting %v-word mnemonic seed separated by a space, followed by an optional password", aezeed.NummnemonicWords)
os.Exit(1)
}
password := flag.String("pass", "", "encryption password")
keystorePath := flag.String("path", defaultKeyStorePath, "path to create keystore dir")
aezeedPassphrase := flag.String("aezeedpass", defaultAezeedPassphrase, "aezeed passphrase")
flag.Parse()
args := flag.Args()

passphrase := defaultPassphrase
if len(os.Args[1:]) > aezeed.NummnemonicWords {
// use provided password
passphrase = []byte(os.Args[25])
if len(args) < aezeed.NummnemonicWords {
fmt.Fprintf(os.Stderr, "\nerror: expecting password and %v-word mnemonic seed separated by spaces\n", aezeed.NummnemonicWords)
os.Exit(1)
}

// parse seed from args
var mnemonic aezeed.Mnemonic
copy(mnemonic[:], os.Args[1:25])
copy(mnemonic[:], args[0:24])

// map back to cipher
cipherSeed, err := mnemonic.ToCipherSeed(passphrase)
aezeedPassphraseBytes := []byte(*aezeedPassphrase)
cipherSeed, err := mnemonic.ToCipherSeed(aezeedPassphraseBytes)
if err != nil {
fmt.Fprint(os.Stderr, "\nerror: invalid seed or password")
fmt.Fprintln(os.Stderr, "\nerror: invalid aezeed:", err)
os.Exit(1)
}

Expand All @@ -58,30 +61,25 @@ func main() {
// perform validations and convert bytes to ecdsa.PrivateKey
privateKey, err := crypto.ToECDSA(masterSecretKey)
if err != nil {
fmt.Fprint(os.Stderr, "\nerror: failed to convert masterSecretKey bytes to ecdsa.PrivateKey")
fmt.Fprintln(os.Stderr, "\nerror: failed to convert masterSecretKey bytes to ecdsa.PrivateKey")
os.Exit(1)
}

// get directory for saving the keystore file
keystorePath := defaultKeyStorePath
if len(os.Args[1:]) > aezeed.NummnemonicWords+1 {
// use use provided path if provided
keystorePath = os.Args[26]
}
dir, err := filepath.Abs(keystorePath)
dir, err := filepath.Abs(filepath.Join(*keystorePath, "keystore"))
if err != nil {
fmt.Fprint(os.Stderr, "\nerror: failed to get directory for keystore")
fmt.Fprintln(os.Stderr, "\nerror: failed to get directory for keystore")
os.Exit(1)
}

// generate keystore file
ks := keystore.NewKeyStore(dir, keystore.StandardScryptN, keystore.StandardScryptP)

// import our ecdsa.PrivateKey to our new keystore
_, err = ks.ImportECDSA(privateKey, string(passphrase))
_, err = ks.ImportECDSA(privateKey, *password)
if err != nil {
fmt.Fprintf(os.Stderr, "\nerror: failed to import key to keystore - %v\n", err)
os.Exit(1)
}
fmt.Print("\nKeystore created.")

fmt.Println("\nKeystore created in", dir)
}

0 comments on commit 977e3d9

Please sign in to comment.