Skip to content

Commit

Permalink
feat: change master password
Browse files Browse the repository at this point in the history
This adds the ability to change the master password of an existing xud
node. Changing the password re-encrypts the node key on disk and queues
password changes for all lnd wallets.

Lnd does not currently offer the ability to change the password of an
unlocked, running instance. Instead lnd can only change its password
right after being started while it is still locked. Xud therefore
saves the old password for each lnd wallet to the xud database and
encrypts the old password using the new passwords. On subsequent
unlocks of xud, when we go to unlock lnd wallets we first check whether
we have any old passwords in the database corresponding to any lnd
wallets. If we do, we decrypt the old password and change the password
for lnd, which in turn will unlock lnd.

Closes #1981.
  • Loading branch information
sangaman committed Nov 27, 2020
1 parent 9c1a9ee commit af6b545
Show file tree
Hide file tree
Showing 21 changed files with 1,142 additions and 416 deletions.
29 changes: 29 additions & 0 deletions docs/api.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions lib/Xud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,16 +119,16 @@ class Xud extends EventEmitter {
const nodeKeyPath = NodeKey.getPath(this.config.xudir, this.config.instanceid);
const nodeKeyExists = await fs.access(nodeKeyPath).then(() => true).catch(() => false);

this.swapClientManager = new SwapClientManager(this.config, loggers, this.unitConverter);
await this.swapClientManager.init(this.db.models);
this.swapClientManager = new SwapClientManager(this.config, loggers, this.unitConverter, this.db.models);
await this.swapClientManager.init();

let nodeKey: NodeKey | undefined;
if (this.config.noencrypt) {
if (nodeKeyExists) {
nodeKey = await NodeKey.fromFile(nodeKeyPath);
} else {
nodeKey = await NodeKey.generate();
await nodeKey.toFile(nodeKeyPath);
nodeKey = await NodeKey.generate(nodeKeyPath);
await nodeKey.toFile();
}

// we need to initialize connext every time xud starts, even in noencrypt mode
Expand Down
14 changes: 7 additions & 7 deletions lib/cli/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,16 +99,16 @@ export const callback = (argv: Arguments, formatOutput?: Function, displayJson?:
}
} else {
const responseObj = response.toObject();
if (Object.keys(responseObj).length === 0) {
console.log('success');
} else {
if (!argv.json && formatOutput) {
formatOutput(responseObj, argv);
if (argv.json || !formatOutput) {
if (Object.keys(responseObj).length === 0) {
console.log('success');
} else {
displayJson
? displayJson(responseObj, argv)
: console.log(JSON.stringify(responseObj, undefined, 2));
? displayJson(responseObj, argv)
: console.log(JSON.stringify(responseObj, undefined, 2));
}
} else {
formatOutput(responseObj, argv);
}
}
};
Expand Down
51 changes: 51 additions & 0 deletions lib/cli/commands/changepass.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import readline from 'readline';
import { Arguments } from 'yargs';
import { ChangePasswordRequest } from '../../proto/xudrpc_pb';
import { callback, loadXudClient } from '../command';

export const command = 'changepass';

export const describe = 'change the password for an existing xud instance';

export const builder = {};

const formatOutput = () => {
console.log('The master xud password was succesfully changed.');
console.log('Passwords for lnd wallets will be changed the next time xud is restarted and unlocked.');
};

export const handler = (argv: Arguments<any>) => {
const rl = readline.createInterface({
input: process.stdin,
terminal: true,
});

console.log(`\
You are changing the master password for xud and underlying wallets.\
`);
process.stdout.write('Enter old password: ');
rl.question('', (oldPassword) => {
process.stdout.write('\nEnter new password: ');
rl.question('', (password1) => {
process.stdout.write('\nRe-enter password: ');
rl.question('', async (password2) => {
process.stdout.write('\n\n');
rl.close();
if (password1 === password2) {
const request = new ChangePasswordRequest();
request.setNewPassword(password1);
request.setOldPassword(oldPassword);

const client = await loadXudClient(argv);
// wait up to 3 seconds for rpc server to listen before call in case xud was just started
client.waitForReady(Date.now() + 3000, () => {
client.changePassword(request, callback(argv, formatOutput));
});
} else {
process.exitCode = 1;
console.error('Passwords do not match, please try again');
}
});
});
});
};
14 changes: 14 additions & 0 deletions lib/grpc/GrpcService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -877,6 +877,20 @@ class GrpcService {
}
}

public changePassword: grpc.handleUnaryCall<xudrpc.ChangePasswordRequest, xudrpc.ChangePasswordResponse> = async (call, callback) => {
if (!this.isReady(this.service, callback)) {
return;
}
try {
await this.service.changePassword(call.request.toObject());

const response = new xudrpc.ChangePasswordResponse();
callback(null, response);
} catch (err) {
callback(getGrpcError(err), null);
}
}

public shutdown: grpc.handleUnaryCall<xudrpc.ShutdownRequest, xudrpc.ShutdownResponse> = (_, callback) => {
if (!this.isReady(this.service, callback)) {
return;
Expand Down
20 changes: 20 additions & 0 deletions lib/lndclient/LndClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ class LndClient extends SwapClient {
public readonly finalLock: number;
public config: LndClientConfig;
public currency: string;
public walletPassword?: string;
private lightning?: LightningClient;
private walletUnlocker?: WalletUnlockerClient;
/** The maximum time to wait for a client to be ready for making grpc calls, can be used for exponential backoff. */
Expand Down Expand Up @@ -950,6 +951,7 @@ class LndClient extends SwapClient {

public initWallet = async (walletPassword: string, seedMnemonic: string[], restore = false, backup?: Uint8Array):
Promise<lndwalletunlocker.InitWalletResponse.AsObject> => {
this.walletPassword = walletPassword;
const request = new lndwalletunlocker.InitWalletRequest();

// from the master seed/mnemonic we derive a child mnemonic for this specific client
Expand Down Expand Up @@ -980,6 +982,7 @@ class LndClient extends SwapClient {
}

public unlockWallet = async (walletPassword: string): Promise<void> => {
this.walletPassword = walletPassword;
const request = new lndwalletunlocker.UnlockWalletRequest();
request.setWalletPassword(Uint8Array.from(Buffer.from(walletPassword, 'utf8')));
await this.unaryWalletUnlockerCall<lndwalletunlocker.UnlockWalletRequest, lndwalletunlocker.UnlockWalletResponse>(
Expand All @@ -989,6 +992,23 @@ class LndClient extends SwapClient {
this.logger.info('wallet unlocked');
}

public changePassword = async (oldPassword: string, newPassword: string) => {
this.walletPassword = newPassword;
const request = new lndwalletunlocker.ChangePasswordRequest();
request.setCurrentPassword(Uint8Array.from(Buffer.from(oldPassword, 'utf8')));
request.setNewPassword(Uint8Array.from(Buffer.from(newPassword, 'utf8')));
await this.unaryWalletUnlockerCall<lndwalletunlocker.ChangePasswordResponse, lndwalletunlocker.ChangePasswordRequest>(
'changePassword', request,
);

// the macaroons change every time lnd changes its password, so we must remove the old one and reload the new one
this.meta.remove('macaroon');
await this.loadMacaroon();

this.setUnlocked();
this.logger.info('password changed & wallet unlocked');
}

public addInvoice = async (
{ rHash, units, expiry = this.finalLock }:
{ rHash: string, units: number, expiry?: number },
Expand Down
21 changes: 13 additions & 8 deletions lib/nodekey/NodeKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,29 +11,31 @@ import { encipher } from '../utils/seedutil';
* and can sign messages to prove their veracity.
*/
class NodeKey {
public password?: string;

/**
* @param privKey The 32 byte private key
* @param pubKey The public key in hex string format.
*/
constructor(public readonly privKey: Buffer, public readonly pubKey: string) { }
constructor(public readonly privKey: Buffer, public readonly pubKey: string, private readonly path: string) { }

/**
* Generates a random NodeKey.
*/
public static generate = async (): Promise<NodeKey> => {
public static generate = async (path?: string): Promise<NodeKey> => {
let privKey: Buffer;
do {
privKey = await randomBytes(32);
} while (!secp256k1.privateKeyVerify(privKey));

return NodeKey.fromBytes(privKey);
return NodeKey.fromBytes(privKey, path);
}

/**
* Converts a buffer of bytes to a NodeKey. Uses the first 32 bytes from the buffer to generate
* the private key. If the buffer has fewer than 32 bytes, the buffer is right-padded with zeros.
*/
public static fromBytes = (bytes: Buffer): NodeKey => {
public static fromBytes = (bytes: Buffer, path?: string): NodeKey => {
let privKey: Buffer;
if (bytes.byteLength === 32) {
privKey = bytes;
Expand All @@ -46,7 +48,7 @@ class NodeKey {
const pubKeyBytes = secp256k1.publicKeyCreate(privKey);
const pubKey = pubKeyBytes.toString('hex');

return new NodeKey(privKey, pubKey);
return new NodeKey(privKey, pubKey, path ?? '');
}

/**
Expand All @@ -66,7 +68,9 @@ class NodeKey {
privKey = fileBuffer;
}
if (secp256k1.privateKeyVerify(privKey)) {
return NodeKey.fromBytes(privKey);
const nodeKey = NodeKey.fromBytes(privKey, path);
nodeKey.password = password;
return nodeKey;
} else {
throw new Error(`${path} does not contain a valid ECDSA private key`);
}
Expand All @@ -92,14 +96,15 @@ class NodeKey {
* @param path the path at which to save the file
* @param password an optional password parameter for encrypting the private key
*/
public toFile = async (path: string, password?: string): Promise<void> => {
public toFile = async (password?: string): Promise<void> => {
let buf: Buffer;
if (password) {
this.password = password;
buf = await encrypt(this.privKey, password);
} else {
buf = this.privKey;
}
await fs.writeFile(path, buf);
await fs.writeFile(this.path, buf);
}

/**
Expand Down
20 changes: 20 additions & 0 deletions lib/proto/xudrpc.swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions lib/proto/xudrpc_grpc_pb.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit af6b545

Please sign in to comment.