Skip to content

Commit 977e3d9

Browse files
committed
fix(seedutil): separate aezeed & encryption pwords
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.
1 parent f32fe6d commit 977e3d9

File tree

3 files changed

+90
-41
lines changed

3 files changed

+90
-41
lines changed

seedutil/README.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
# seedutil
2+
23
This utility is used to derive an Ethereum keystore file from [aezeed](https://github.com/lightningnetwork/lnd/tree/master/aezeed) generated mnemonic seed.
34

45
## Build
6+
57
`npm run compile:seedutil`
68

79
## Usage
10+
811
It is recommended to use this tool on the command line ONLY for development purposes.
9-
`seedutil [twenty four recovery words separated by space] [optional password] [optional keystore path]`
1012

11-
[Tests](/test/jest/SeedUtil.spec.ts)
13+
`seedutil [-pass=encryption password] [-path=optional/keystore/path] [-aezeedpass=optional_seed_pass] <twenty four recovery words separated by spaces>`
14+
15+
By default the `keystore` folder will be created in the execution directory and the aezeed password will be `aezeed`.
16+
17+
## Tests
18+
19+
`npm run test:seedutil`

seedutil/SeedUtil.spec.ts

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { exec } from 'child_process';
2-
import { promises as fs, existsSync } from 'fs';
2+
import { promises as fs, existsSync, lstatSync } from 'fs';
33

44
const executeCommand = (cmd: string): Promise<string> => {
55
return new Promise((resolve, reject) => {
@@ -25,7 +25,11 @@ const deleteDir = async (path: string) => {
2525
const files = await fs.readdir(path);
2626
const deletePromises: Promise<void>[] = [];
2727
files.forEach((file) => {
28-
deletePromises.push(fs.unlink(`${path}/${file}`));
28+
if (lstatSync(`${path}/${file}`).isDirectory()) { // recurse
29+
deletePromises.push(deleteDir(`${path}/${file}`));
30+
} else { // delete file
31+
deletePromises.push(fs.unlink(`${path}/${file}`));
32+
}
2933
});
3034
await Promise.all(deletePromises);
3135
// delete directory
@@ -38,11 +42,14 @@ const deleteDir = async (path: string) => {
3842

3943
const SUCCESS_KEYSTORE_CREATED = 'Keystore created';
4044
const ERRORS = {
41-
INVALID_MNEMONIC_LENGTH: 'expecting 24-word mnemonic seed separated by a space',
42-
INVALID_SEED_OR_PASSWORD: 'invalid seed or password',
45+
INVALID_ARGS_LENGTH: 'expecting password and 24-word mnemonic seed separated by spaces',
46+
MISSING_ENCRYPTION_PASSWORD: 'expecting encryption password',
47+
INVALID_AEZEED: 'invalid aezeed',
4348
KEYSTORE_FILE_ALREADY_EXISTS: 'account already exists',
4449
};
4550

51+
const PASSWORD = 'wasspord';
52+
4653
const VALID_SEED = {
4754
seedPassword: 'mysecretpassword',
4855
seedWords: [
@@ -54,6 +61,16 @@ const VALID_SEED = {
5461
ethAddress: '23ccdcd149bd433d64987ffebbc88ac909842303',
5562
};
5663

64+
const VALID_SEED_NO_PASS = {
65+
seedWords: [
66+
'abstract', 'swear', 'air', 'swamp', 'carpet', 'that',
67+
'retire', 'pool', 'produce', 'food', 'join', 'inform',
68+
'giraffe', 'local', 'region', 'anchor', 'march', 'advice',
69+
'blanket', 'quick', 'farm', 'mandate', 'shell', 'lens',
70+
],
71+
ethAddress: 'e650ced4be22e305bd133a0b7f8e50b9c5568c57',
72+
};
73+
5774
const DEFAULT_KEYSTORE_PATH = `${process.cwd()}/seedutil/keystore`;
5875

5976
describe('SeedUtil', () => {
@@ -63,23 +80,23 @@ describe('SeedUtil', () => {
6380

6481
test('it errors with no arguments', async () => {
6582
await expect(executeCommand('./seedutil/seedutil'))
66-
.rejects.toThrow(ERRORS.INVALID_MNEMONIC_LENGTH);
83+
.rejects.toThrow(ERRORS.INVALID_ARGS_LENGTH);
6784
});
6885

6986
test('it errors with 23 words', async () => {
70-
const cmd = `./seedutil/seedutil ${VALID_SEED.seedWords.slice(0, 22).join(' ')}`;
87+
const cmd = `./seedutil/seedutil ${VALID_SEED.seedWords.slice(0, 23).join(' ')}`;
7188
await expect(executeCommand(cmd))
72-
.rejects.toThrow(ERRORS.INVALID_MNEMONIC_LENGTH);
89+
.rejects.toThrow(ERRORS.INVALID_ARGS_LENGTH);
7390
});
7491

75-
test('it errors with 24 words and invalid password', async () => {
92+
test('it errors with 24 words and invalid aezeed password', async () => {
7693
const cmd = `./seedutil/seedutil ${VALID_SEED.seedWords.join(' ')}`;
7794
await expect(executeCommand(cmd))
78-
.rejects.toThrow(ERRORS.INVALID_SEED_OR_PASSWORD);
95+
.rejects.toThrow(ERRORS.INVALID_AEZEED);
7996
});
8097

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

115+
test('it succeeds with 24 words, no aezeed password', async () => {
116+
const cmd = `./seedutil/seedutil ${VALID_SEED_NO_PASS.seedWords.join(' ')}`;
117+
await expect(executeCommand(cmd))
118+
.resolves.toMatch(SUCCESS_KEYSTORE_CREATED);
119+
// Read our keystore file
120+
const files = await fs.readdir(DEFAULT_KEYSTORE_PATH);
121+
expect(files.length).toEqual(1);
122+
const keyStorePath = `${DEFAULT_KEYSTORE_PATH}/${files[0]}`;
123+
const keyStoreObj = JSON.parse(await fs.readFile(keyStorePath, 'utf8'));
124+
// verify that the derived ETH address matches
125+
expect(keyStoreObj.address).toEqual(VALID_SEED_NO_PASS.ethAddress);
126+
});
127+
128+
test('it succeeds with 24 word and encryption password', async () => {
129+
const cmd = `./seedutil/seedutil -pass=${PASSWORD} ${VALID_SEED_NO_PASS.seedWords.join(' ')}`;
130+
await expect(executeCommand(cmd))
131+
.resolves.toMatch(SUCCESS_KEYSTORE_CREATED);
132+
// Read our keystore file
133+
const files = await fs.readdir(DEFAULT_KEYSTORE_PATH);
134+
expect(files.length).toEqual(1);
135+
const keyStorePath = `${DEFAULT_KEYSTORE_PATH}/${files[0]}`;
136+
const keyStoreObj = JSON.parse(await fs.readFile(keyStorePath, 'utf8'));
137+
// verify that the derived ETH address matches
138+
expect(keyStoreObj.address).toEqual(VALID_SEED_NO_PASS.ethAddress);
139+
});
140+
98141
test('it allows custom keystore save path', async () => {
99142
const CUSTOM_PATH = `${process.cwd()}/seedutil/custom`;
100-
const cmd = `./seedutil/seedutil ${VALID_SEED.seedWords.join(' ')} ${VALID_SEED.seedPassword} ${CUSTOM_PATH}`;
143+
const cmd = `./seedutil/seedutil -path=${CUSTOM_PATH} -aezeedpass=${VALID_SEED.seedPassword} ${VALID_SEED.seedWords.join(' ')}`;
101144
await expect(executeCommand(cmd))
102145
.resolves.toMatch(SUCCESS_KEYSTORE_CREATED);
103146
// cleanup custom path

seedutil/main.go

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,46 +3,49 @@ package main
33
import (
44
"crypto/hmac"
55
"crypto/sha512"
6+
"flag"
67
"fmt"
8+
"os"
9+
"path/filepath"
10+
711
"github.com/ethereum/go-ethereum/accounts/keystore"
812
"github.com/ethereum/go-ethereum/crypto"
913
"github.com/lightningnetwork/lnd/aezeed"
10-
"os"
11-
"path/filepath"
1214
)
1315

1416
var (
1517
// defaultPassphrase is the default passphrase that will
16-
// be used for decryption
17-
defaultPassphrase = []byte("aezeed")
18+
// be used for the seed
19+
defaultAezeedPassphrase = "aezeed"
1820
// masterKey is the master key used along with a random seed used to generate
1921
// the master node in the hierarchical tree.
2022
masterKey = []byte("Bitcoin seed")
2123
// by default we will generate the keystore file into the keystore directory
2224
// relative to the execution directory
23-
defaultKeyStorePath = filepath.Join(filepath.Dir(os.Args[0]), "keystore")
25+
defaultKeyStorePath = filepath.Join(filepath.Dir(os.Args[0]))
2426
)
2527

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

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

3840
// parse seed from args
3941
var mnemonic aezeed.Mnemonic
40-
copy(mnemonic[:], os.Args[1:25])
42+
copy(mnemonic[:], args[0:24])
4143

4244
// map back to cipher
43-
cipherSeed, err := mnemonic.ToCipherSeed(passphrase)
45+
aezeedPassphraseBytes := []byte(*aezeedPassphrase)
46+
cipherSeed, err := mnemonic.ToCipherSeed(aezeedPassphraseBytes)
4447
if err != nil {
45-
fmt.Fprint(os.Stderr, "\nerror: invalid seed or password")
48+
fmt.Fprintln(os.Stderr, "\nerror: invalid aezeed:", err)
4649
os.Exit(1)
4750
}
4851

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

65-
// get directory for saving the keystore file
66-
keystorePath := defaultKeyStorePath
67-
if len(os.Args[1:]) > aezeed.NummnemonicWords+1 {
68-
// use use provided path if provided
69-
keystorePath = os.Args[26]
70-
}
71-
dir, err := filepath.Abs(keystorePath)
68+
dir, err := filepath.Abs(filepath.Join(*keystorePath, "keystore"))
7269
if err != nil {
73-
fmt.Fprint(os.Stderr, "\nerror: failed to get directory for keystore")
70+
fmt.Fprintln(os.Stderr, "\nerror: failed to get directory for keystore")
7471
os.Exit(1)
7572
}
7673

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

8077
// import our ecdsa.PrivateKey to our new keystore
81-
_, err = ks.ImportECDSA(privateKey, string(passphrase))
78+
_, err = ks.ImportECDSA(privateKey, *password)
8279
if err != nil {
8380
fmt.Fprintf(os.Stderr, "\nerror: failed to import key to keystore - %v\n", err)
8481
os.Exit(1)
8582
}
86-
fmt.Print("\nKeystore created.")
83+
84+
fmt.Println("\nKeystore created in", dir)
8785
}

0 commit comments

Comments
 (0)