-
Notifications
You must be signed in to change notification settings - Fork 272
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(express): lightning wallet creation step two
initialize signer lnd and update platform. Ticket: BTC-1356
- Loading branch information
1 parent
b293ed9
commit 5bc7584
Showing
22 changed files
with
882 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import * as t from 'io-ts'; | ||
import { NonEmptyString } from 'io-ts-types'; | ||
import { IPCustomCodec } from '@bitgo/sdk-core'; | ||
|
||
export const WalletStateCodec = t.keyof({ | ||
NON_EXISTING: 1, | ||
LOCKED: 1, | ||
UNLOCKED: 1, | ||
RPC_ACTIVE: 1, | ||
SERVER_ACTIVE: 1, | ||
WAITING_TO_START: 1, | ||
}); | ||
|
||
export type WalletState = t.TypeOf<typeof WalletStateCodec>; | ||
|
||
export const LightningSignerConfigCodec = t.type({ | ||
url: NonEmptyString, | ||
tlsCert: NonEmptyString, | ||
}); | ||
|
||
export type LightningSignerConfig = t.TypeOf<typeof LightningSignerConfigCodec>; | ||
|
||
export const LightningSignerConfigsCodec = t.record(t.string, LightningSignerConfigCodec); | ||
|
||
export type LightningSignerConfigs = t.TypeOf<typeof LightningSignerConfigsCodec>; | ||
|
||
export const GetWalletStateResponseCodec = t.type( | ||
{ | ||
state: WalletStateCodec, | ||
}, | ||
'GetWalletStateResponse' | ||
); | ||
|
||
export type GetWalletStateResponse = t.TypeOf<typeof GetWalletStateResponseCodec>; | ||
|
||
export const InitLightningWalletRequestCodec = t.strict( | ||
{ | ||
walletId: NonEmptyString, | ||
passphrase: NonEmptyString, | ||
signerIP: IPCustomCodec, | ||
signerTlsCert: NonEmptyString, | ||
signerTlsKey: NonEmptyString, | ||
expressIP: IPCustomCodec, | ||
}, | ||
'InitLightningWalletRequest' | ||
); | ||
|
||
export type InitLightningWalletRequest = t.TypeOf<typeof InitLightningWalletRequestCodec>; | ||
|
||
export const CreateSignerMacaroonRequestCodec = t.strict( | ||
{ | ||
walletId: NonEmptyString, | ||
passphrase: NonEmptyString, | ||
watchOnlyIP: IPCustomCodec, | ||
}, | ||
'CreateSignerMacaroonRequest' | ||
); | ||
|
||
export type CreateSignerMacaroonRequest = t.TypeOf<typeof CreateSignerMacaroonRequestCodec>; | ||
|
||
export const InitWalletResponseCodec = t.type( | ||
{ | ||
admin_macaroon: NonEmptyString, | ||
}, | ||
'InitWalletResponse' | ||
); | ||
|
||
export type InitWalletResponse = t.TypeOf<typeof InitWalletResponseCodec>; | ||
|
||
export const BakeMacaroonResponseCodec = t.type( | ||
{ | ||
macaroon: NonEmptyString, | ||
}, | ||
'BakeMacaroonResponse' | ||
); | ||
|
||
export type BakeMacaroonResponse = t.TypeOf<typeof BakeMacaroonResponseCodec>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
import * as express from 'express'; | ||
import { | ||
decodeOrElse, | ||
createMessageSignature, | ||
getUtxolibNetwork, | ||
signerMacaroonPermissions, | ||
createWatchOnly, | ||
addIPCaveatToMacaroon, | ||
} from '@bitgo/sdk-core'; | ||
import * as utxolib from '@bitgo/utxo-lib'; | ||
import * as https from 'https'; | ||
import { Buffer } from 'buffer'; | ||
|
||
import { CreateSignerMacaroonRequestCodec, GetWalletStateResponse, InitLightningWalletRequestCodec } from './codecs'; | ||
import { getLightningSignerConfig } from './lightningUtils'; | ||
import { bakeMacaroon, createHttpAgent, getWalletState, initWallet } from './signerClient'; | ||
import { getLightningAuthKeychains, getLightningKeychain, updateWallet } from './lightningWallets'; | ||
|
||
type Decrypt = (params: { input: string; password: string }) => string; | ||
|
||
async function createSignerMacaroon( | ||
config: { url: string; httpsAgent: https.Agent }, | ||
header: { adminMacaroonHex: string }, | ||
watchOnlyIP: string | ||
) { | ||
const { macaroon } = await bakeMacaroon(config, header, { permissions: signerMacaroonPermissions }); | ||
const macaroonBase64 = addIPCaveatToMacaroon(Buffer.from(macaroon, 'hex').toString('base64'), watchOnlyIP); | ||
return Buffer.from(macaroonBase64, 'base64').toString('hex'); | ||
} | ||
|
||
function getSignerRootKey( | ||
passphrase: string, | ||
userMainnetEncryptedPrv: string, | ||
network: utxolib.Network, | ||
decrypt: Decrypt | ||
) { | ||
const userMainnetPrv = decrypt({ password: passphrase, input: userMainnetEncryptedPrv }); | ||
return utxolib.bitgo.keyutil.changeExtendedKeyNetwork(userMainnetPrv, utxolib.networks.bitcoin, network); | ||
} | ||
|
||
function getMacaroonRootKey(passphrase: string, nodeAuthEncryptedPrv: string, decrypt: Decrypt) { | ||
const hdNode = utxolib.bip32.fromBase58(decrypt({ password: passphrase, input: nodeAuthEncryptedPrv })); | ||
if (!hdNode.privateKey) { | ||
throw new Error('nodeAuthEncryptedPrv is not a private key'); | ||
} | ||
return hdNode.privateKey.toString('base64'); | ||
} | ||
|
||
export async function handleInitLightningWallet(req: express.Request): Promise<unknown> { | ||
const { walletId, passphrase, signerTlsKey, signerTlsCert, signerIP, expressIP } = decodeOrElse( | ||
InitLightningWalletRequestCodec.name, | ||
InitLightningWalletRequestCodec, | ||
req.body, | ||
(_) => { | ||
// DON'T throw errors from decodeOrElse. It could leak sensitive information. | ||
throw new Error('Invalid request body to initialise lightning wallet'); | ||
} | ||
); | ||
|
||
const { url, tlsCert } = await getLightningSignerConfig(walletId, req.config); | ||
|
||
const bitgo = req.bitgo; | ||
const coin = bitgo.coin(req.params.coin); | ||
if (coin.getFamily() !== 'lnbtc') { | ||
throw new Error('Invalid coin to initialise lightning wallet'); | ||
} | ||
|
||
const wallet = await coin.wallets().get({ id: walletId }); | ||
|
||
const userKey = await getLightningKeychain(coin, wallet); | ||
const { userAuthKey, nodeAuthKey } = await getLightningAuthKeychains(coin, wallet); | ||
|
||
const network = getUtxolibNetwork(coin.getChain()); | ||
const signerRootKey = getSignerRootKey(passphrase, userKey.encryptedPrv, network, bitgo.decrypt); | ||
const macaroonRootKey = getMacaroonRootKey(passphrase, nodeAuthKey.encryptedPrv, bitgo.decrypt); | ||
|
||
const httpsAgent = createHttpAgent(tlsCert); | ||
const { admin_macaroon: adminMacaroon } = await initWallet( | ||
{ url, httpsAgent }, | ||
{ | ||
wallet_password: passphrase, | ||
extended_master_key: signerRootKey, | ||
macaroon_root_key: macaroonRootKey, | ||
} | ||
); | ||
|
||
const encryptedAdminMacaroon = bitgo.encrypt({ | ||
password: passphrase, | ||
input: addIPCaveatToMacaroon(adminMacaroon, expressIP), | ||
}); | ||
const encryptedSignerTlsKey = bitgo.encrypt({ password: passphrase, input: signerTlsKey }); | ||
const watchOnly = createWatchOnly(signerRootKey, network); | ||
|
||
const coinSpecific = { | ||
[coin.getChain()]: { | ||
encryptedAdminMacaroon, | ||
signerIP, | ||
signerTlsCert, | ||
encryptedSignerTlsKey, | ||
watchOnly, | ||
}, | ||
}; | ||
|
||
const signature = createMessageSignature( | ||
coinSpecific, | ||
bitgo.decrypt({ password: passphrase, input: userAuthKey.encryptedPrv }) | ||
); | ||
|
||
return await updateWallet(bitgo, wallet, { coinSpecific, signature }); | ||
} | ||
|
||
export async function handleCreateSignerMacaroon(req: express.Request): Promise<unknown> { | ||
const { walletId, passphrase, watchOnlyIP } = decodeOrElse( | ||
CreateSignerMacaroonRequestCodec.name, | ||
CreateSignerMacaroonRequestCodec, | ||
req.body, | ||
(_) => { | ||
// DON'T throw errors from decodeOrElse. It could leak sensitive information. | ||
throw new Error('Invalid request body for CreateSignerMacaroon.'); | ||
} | ||
); | ||
|
||
const { url, tlsCert } = await getLightningSignerConfig(walletId, req.config); | ||
|
||
const bitgo = req.bitgo; | ||
const coin = bitgo.coin(req.params.coin); | ||
if (coin.getFamily() !== 'lnbtc') { | ||
throw new Error('Invalid coin for CreateSignerMacaroon'); | ||
} | ||
|
||
const wallet = await coin.wallets().get({ id: walletId }); | ||
|
||
const encryptedAdminMacaroon = wallet.coinSpecific()?.encryptedAdminMacaroon; | ||
if (!encryptedAdminMacaroon) { | ||
throw new Error('Missing encryptedAdminMacaroon in wallet'); | ||
} | ||
const adminMacaroon = bitgo.decrypt({ password: passphrase, input: encryptedAdminMacaroon }); | ||
|
||
const { userAuthKey } = await getLightningAuthKeychains(coin, wallet); | ||
|
||
const httpsAgent = createHttpAgent(tlsCert); | ||
const signerMacaroon = await createSignerMacaroon( | ||
{ url, httpsAgent }, | ||
{ adminMacaroonHex: Buffer.from(adminMacaroon, 'base64').toString('hex') }, | ||
watchOnlyIP | ||
); | ||
|
||
// TODO BTC-1465 - Encrypt the signer macaroon using ECDH with the user and LS key pairs | ||
const coinSpecific = { | ||
[coin.getChain()]: { | ||
signerMacaroon, | ||
}, | ||
}; | ||
|
||
const signature = createMessageSignature( | ||
coinSpecific, | ||
bitgo.decrypt({ password: passphrase, input: userAuthKey.encryptedPrv }) | ||
); | ||
|
||
return await updateWallet(bitgo, wallet, { coinSpecific, signature }); | ||
} | ||
|
||
export async function handleGetLightningWalletState(req: express.Request): Promise<GetWalletStateResponse> { | ||
const coin = req.bitgo.coin(req.params.coin); | ||
if (coin.getFamily() !== 'lnbtc') { | ||
throw new Error('Invalid coin for lightning wallet state'); | ||
} | ||
|
||
const { url, tlsCert } = await getLightningSignerConfig(req.params.id, req.config); | ||
const httpsAgent = createHttpAgent(tlsCert); | ||
return await getWalletState({ url, httpsAgent }); | ||
} |
Oops, something went wrong.