diff --git a/.gitignore b/.gitignore index ea8c4bf..3fa0a4e 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ +.vscode /target +*.psbt \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 59dfb22..6692116 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,23 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "anyhow" +version = "1.0.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602" + +[[package]] +name = "async-trait" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76464446b8bc32758d7e88ee1a804d9914cd9b1cb264c029899680b0be29826f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "atty" version = "0.2.14" @@ -13,30 +30,106 @@ dependencies = [ "winapi", ] +[[package]] +name = "autocfg" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dde43e75fd43e8a1bf86103336bc699aa8d17ad1be60c76c0bdfd4828e19b78" +dependencies = [ + "autocfg 1.1.0", +] + [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "base64-compat" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a8d4d2746f89841e49230dd26917df1876050f95abafafbe34f47cb534b88d7" +dependencies = [ + "byteorder", +] + +[[package]] +name = "bdk" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14836e8b1312be32e46f5da47e1189c2239fa75f9237a0a7c7aea9ee7071a2bc" +dependencies = [ + "async-trait", + "bdk-macros", + "bip39", + "bitcoin", + "js-sys", + "log", + "miniscript", + "rand 0.7.3", + "serde", + "serde_json", + "tokio", +] + +[[package]] +name = "bdk-macros" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81c1980e50ae23bb6efa9283ae8679d6ea2c6fa6a99fe62533f65f4a25a1a56c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bech32" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9ff0bbfd639f15c74af777d81383cf53efb7c93613f6cab67c6c11e05bbf8b" + [[package]] name = "bip39" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e89470017230c38e52b82b3ee3f530db1856ba1d434e3a67a3456a8a8dec5f" dependencies = [ - "bitcoin_hashes", - "rand_core", + "bitcoin_hashes 0.9.7", + "rand_core 0.4.2", "serde", "unicode-normalization", ] +[[package]] +name = "bitcoin" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05bba324e6baf655b882df672453dbbc527bc938cadd27750ae510aaccc3a66a" +dependencies = [ + "base64-compat", + "bech32", + "bitcoin_hashes 0.10.0", + "secp256k1", + "serde", +] + [[package]] name = "bitcoin_hashes" version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ce18265ec2324ad075345d5814fbeed4f41f0a660055dc78840b74d19b874b1" +[[package]] +name = "bitcoin_hashes" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "006cc91e1a1d99819bc5b8214be3555c1f0611b169f527a1fdc54ed1f2b745b0" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -54,14 +147,33 @@ dependencies = [ [[package]] name = "brainseed" -version = "0.3.0" +version = "0.4.0" dependencies = [ - "bip39", + "anyhow", + "bdk", "clap", "rpassword", "sha2", ] +[[package]] +name = "bumpalo" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "cc" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" + [[package]] name = "cfg-if" version = "1.0.0" @@ -107,6 +219,15 @@ dependencies = [ "os_str_bytes", ] +[[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +dependencies = [ + "bitflags", +] + [[package]] name = "cpufeatures" version = "0.2.4" @@ -136,6 +257,12 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + [[package]] name = "generic-array" version = "0.14.6" @@ -146,6 +273,19 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -173,22 +313,56 @@ version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" dependencies = [ - "autocfg", + "autocfg 1.1.0", "hashbrown", ] +[[package]] +name = "itoa" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" + +[[package]] +name = "js-sys" +version = "0.3.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "libc" version = "0.2.132" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + [[package]] name = "maybe-uninit" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" +[[package]] +name = "miniscript" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da39fc7a8adea97a677337b0091779dd86349226b869053af496584a9b9e5847" +dependencies = [ + "bitcoin", + "serde", +] + [[package]] name = "once_cell" version = "1.13.1" @@ -201,6 +375,18 @@ version = "6.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "ppv-lite86" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -243,12 +429,162 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" +dependencies = [ + "autocfg 0.1.8", + "libc", + "rand_chacha 0.1.1", + "rand_core 0.4.2", + "rand_hc 0.1.0", + "rand_isaac", + "rand_jitter", + "rand_os", + "rand_pcg", + "rand_xorshift", + "winapi", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc 0.2.0", +] + +[[package]] +name = "rand_chacha" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" +dependencies = [ + "autocfg 0.1.8", + "rand_core 0.3.1", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + [[package]] name = "rand_core" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_hc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_isaac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rand_jitter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" +dependencies = [ + "libc", + "rand_core 0.4.2", + "winapi", +] + +[[package]] +name = "rand_os" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" +dependencies = [ + "cloudabi", + "fuchsia-cprng", + "libc", + "rand_core 0.4.2", + "rdrand", + "winapi", +] + +[[package]] +name = "rand_pcg" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" +dependencies = [ + "autocfg 0.1.8", + "rand_core 0.4.2", +] + +[[package]] +name = "rand_xorshift" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + [[package]] name = "rpassword" version = "7.0.0" @@ -259,11 +595,62 @@ dependencies = [ "winapi", ] +[[package]] +name = "ryu" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" + +[[package]] +name = "secp256k1" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26947345339603ae8395f68e2f3d85a6b0a8ddfe6315818e80b8504415099db0" +dependencies = [ + "rand 0.6.5", + "secp256k1-sys", + "serde", +] + +[[package]] +name = "secp256k1-sys" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "152e20a0fd0519390fc43ab404663af8a0b794273d2a91d60ad4a39f13ffe110" +dependencies = [ + "cc", +] + [[package]] name = "serde" version = "1.0.144" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.144" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" +dependencies = [ + "itoa", + "ryu", + "serde", +] [[package]] name = "sha2" @@ -317,6 +704,16 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" +[[package]] +name = "tokio" +version = "1.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099" +dependencies = [ + "autocfg 1.1.0", + "pin-project-lite", +] + [[package]] name = "typenum" version = "1.15.0" @@ -344,6 +741,66 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasm-bindgen" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 0d08a2f..f237ed5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,13 @@ [package] name = "brainseed" -version = "0.3.0" +version = "0.4.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -bip39 = "1.0.1" +anyhow = "1.0.65" +bdk = { version = "0.22.0", default-features = false, features = ["keys-bip39"] } clap = { version = "3.2.17", features = ["derive"] } rpassword = "7.0.0" sha2 = "0.10.2" diff --git a/README.md b/README.md index 2d39427..051be27 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,42 @@ # brainseed -`brainseed` is a terminal utility to generate a BIP-39 compatible seed phrase from deterministic "entropy". It accepts a passphrase and uses that passphrase as the input for an algorithm to deterministically create a 12 (or 24) word BIP-39 seed phrase. +`brainseed` is a terminal utility for working with [BIP-32 wallets](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) that stem from a passphrase +instead of a mnemonic seed phrase. +**WARNING**: Please note this is experimental software at this time. It is very simple, but be warned, there could be bugs lurking, and the algorithm may change based on feedback. + +## Generating a seed phrase mnemonic -```shell -$ brainseed "hello world" +``` +$ brainseed seed +Entropy prompt: hello world +Confirm: hello world cliff burden nut payment negative soccer one mad pulse balcony force inside ``` -**WARNING**: Please note this is experimental software at this time. It is very simple, but be warned, there could be bugs lurking, and the algorithm may change based on feedback. +This is deterministic function. Repeat multiple times to verify! By default, this uses ten million SHA-256 iterations, but you are encouraged to customize that using the `-n` flag. + +## Watch-only wallet descriptor + +Directly generate a watch-only output descriptor, to import into a wallet like Sparrow or Blue Wallet: + +``` +$ brainseed watch +Entropy prompt: hello world +Confirm: hello world +wpkh([b343958c/84'/1'/0']tpubDCCj6osNwAnnSqViJQjqHD9xkJ6UXKT73ZB36W5gNapyCmirdibyzHeRsAYK5z9V5fi4ZdGAGA2jbXxPD1qS3Yht2tU3shPuatfUUWvKeCc/0/*)#cj35ttn3 +``` +## Signing a transaction + +This can be used to sign files as well, if you don't want to use a seed phrase in a separate wallet, and instead just want to use a watch-only wallet as above, combined with this function to act as a software signing device. + +``` +$ brainseed sign input.psbt output.psbt +Entropy prompt: hello world +Confirm: hello world +``` + +After importing a watch-only wallet into something like Sparrow, generate a transaction and save it as a raw file. Then sign it with this command, and load it back into Sparrow. ## Frequently Asked Questions @@ -24,7 +52,7 @@ Similar to [Border Wallets](https://www.borderwallets.com), in a case where you ### Why does it generate a BIP-39 seed phrase? -BIP-39 seed phrases have become the lingua franca of Bitcoin key management. Almost every wallet allows creating/importing BIP-39 seed phrases, so deterministically generating these seed phrases makes sense for compatibility. +BIP-39 seed phrases have become the lingua franca of Bitcoin key management. Almost every wallet allows creating/importing BIP-39 seed phrases, so deterministically generating these seed phrases makes sense for wallet compatibility. ### How does it work, technically? @@ -36,7 +64,7 @@ After providing a passphrase to the utility it: ### Is this not poor security? -Well, humans are relatively predictable, so it won't stand up to brute force attacks like a random seed mnemonic will. On the other hand, it might also be better opsec to have a passphrase that is hard to forget and only in your head, instead of a random phrase that you have to keep a physical copy just to remember. +Well, humans are relatively predictable, so it won't stand up to brute force attacks like a random seed mnemonic will. On the other hand, it might also be better opsec to have a passphrase that is hard to forget and only in your head, instead of a random mnemonic that you have to keep a physical copy just to remember. In a pinch, it may be a good way to flee a hostile area with your wealth intact. @@ -48,11 +76,4 @@ Use the `-l` or `--long` flag to get a 24 word seed phrase. ### What about rainbow tables? -Yup, that's a danger. Use a phrase meaningful to you, not a famous movie line or something like that. Also consider using a custom number of SHA-256 iterations as this will help foil rainbow attacks. - -If you absolutely must use a famous movie line, then salt it with some other meaningful data, like the year you lost your viriginity, e.g.: - -```shell -$ brainseed "there's no crying in baseball never" -merit permit chef reveal month pizza elbow cheap actual under cargo march -``` \ No newline at end of file +Yup, that's a danger. Use a phrase meaningful to you, not a famous movie line or something like that. Also consider using a custom number of SHA-256 iterations as this will help foil rainbow attacks. \ No newline at end of file diff --git a/src/cli.rs b/src/cli.rs index aa99dde..3cbdfb4 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,71 +1,125 @@ use std::{io::Write, path::PathBuf}; -use crate::util::exit_with_error; +use anyhow::Context; +use bdk::{ + bitcoin::{ + consensus::{deserialize, serialize}, + psbt::PartiallySignedTransaction, + Network, + }, + database::MemoryDatabase, + keys::{bip39::Mnemonic, DerivableKey, ExtendedKey}, + miniscript::miniscript, + template::Bip84, + KeychainKind, Wallet, +}; + +use crate::generator::Generator; #[derive(clap::Parser, Clone)] pub struct Cli { - #[clap( - value_parser, - help = "Input string to use as entropy for BIP-32 seed phrase" - )] - pub input: Option, - - #[clap(short, long, help = "Use a file as input instead of command line")] - pub file: Option, - - #[clap( - short = 'n', - default_value = "10000000", - help = "Number of times to hash the passphrase" - )] + /// Number of times to hash the passphrase + #[clap(short = 'n', default_value = "10000000")] pub iterations: usize, - #[clap(short, long, help = "Return a 24 word seed phrase [default: 12]")] - pub long: bool, - - #[clap(short, long, help = "Prompt for passphrase instead of shell argument")] - pub prompt: bool, + /// Return a 24 word seed phrase [default: 12] + #[clap(short, long)] + long: bool, - #[clap(short, long, help = "Output to file")] - pub output: Option, + #[clap(subcommand)] + pub action: Action, } impl Cli { - pub fn get_input(&self) -> Vec { - let data = if let Some(input) = &self.input { - input.as_bytes().to_vec() - } else if let Some(file) = &self.file { - if let Ok(contents) = std::fs::read(file) { - contents - } else { - exit_with_error("Unable to read file."); - } - } else if self.prompt { - if let Ok((true, pass)) = self.get_password() { - pass.as_bytes().to_vec() - } else { - exit_with_error("Entropy prompt does not match."); - } - } else { - exit_with_error("No input given."); - }; + pub fn exec(&self) -> anyhow::Result<()> { + let input = self.get_input()?; + let seed = self.seed(input); + match &self.action { + Action::Seed => self.write_output(seed.to_string().as_bytes()), + Action::Sign { input, output } => self.sign(input, output, seed), + Action::Watch => self.show_descriptor(seed), + } + } - data + fn seed(&self, input: Vec) -> Mnemonic { + let mut gen = Generator { + input, + iterations: self.iterations, + long: self.long, + }; + gen.seed() } - pub fn write_output(&self, data: &[u8]) { - if let Some(path) = &self.output { - if let Err(e) = std::fs::write(path, data) { - exit_with_error(&format!("Error writing output file: {e}")); - } - } else { - std::io::stdout().write(data).ok(); + fn sign(&self, input: &PathBuf, output: &PathBuf, seed: Mnemonic) -> anyhow::Result<()> { + let wallet = self.wallet(seed)?; + let input = std::fs::read(&input).context("Unable to read signing input file")?; + let mut psbt: PartiallySignedTransaction = + deserialize(&input).context("Failed to deserialize partially signed transaction")?; + let signed = wallet + .sign(&mut psbt, Default::default()) + .context("Error encountered signing the transaction")?; + let s = serialize(&psbt); + std::fs::write(output, &s).context("Unable to write signed transaction")?; + + if !signed { + println!("Transaction not complete."); } + + Ok(()) } - fn get_password(&self) -> std::io::Result<(bool, String)> { - let pass = rpassword::prompt_password("Entropy phrase: ")?; + fn wallet(&self, seed: Mnemonic) -> anyhow::Result> { + let xkey: ExtendedKey = seed + .into_extended_key() + .context("Failed to convert mnemonic into an extended key")?; + let xprv = xkey.into_xprv(Network::Testnet).unwrap(); + Wallet::new( + Bip84(xprv.clone(), KeychainKind::External), + Some(Bip84(xprv.clone(), KeychainKind::External)), + Network::Testnet, + MemoryDatabase::default(), + ) + .context("Failed to create wallet") + .into() + } + + fn get_input(&self) -> anyhow::Result> { + Ok(self.get_password()?.as_bytes().to_vec()) + } + + fn write_output(&self, data: &[u8]) -> anyhow::Result<()> { + std::io::stdout().write(data)?; + Ok(()) + } + + fn get_password(&self) -> anyhow::Result { + let pass = rpassword::prompt_password("Entropy prompt: ")?; let confirm = rpassword::prompt_password("Confirm: ")?; - Ok((pass == confirm, pass)) + if pass == confirm { + return Ok(pass); + } + Err(anyhow::anyhow!("Prompt does not match")) + } + + fn show_descriptor(&self, seed: Mnemonic) -> anyhow::Result<()> { + let wallet = self.wallet(seed)?; + let descriptor = wallet + .public_descriptor(KeychainKind::External) + .context("Descriptor error")? + .ok_or(anyhow::anyhow!("Descriptor error"))?; + print!("{descriptor}"); + Ok(()) } } + +#[derive(clap::Subcommand, Clone)] +pub enum Action { + /// Generate a mnemonic seed phrase. + Seed, + + /// Sign a bitcoin transaction file. + Sign { input: PathBuf, output: PathBuf }, + + /// Show wallet descriptor that is useful for importing as a watch-only wallet. + Watch, +} diff --git a/src/generator.rs b/src/generator.rs index ec37b79..a08331f 100644 --- a/src/generator.rs +++ b/src/generator.rs @@ -1,51 +1,39 @@ -use bip39::Mnemonic; +use bdk::keys::bip39::Mnemonic; use sha2::{Digest, Sha256}; -use crate::cli::Cli; - pub struct Generator { - data: Vec, - iterations: usize, - long: bool, + pub input: Vec, + pub iterations: usize, + pub long: bool, } impl Generator { /// This is the entry point for the struct. pub fn seed(&mut self) -> Mnemonic { self.hash_iterations(); - bip39::Mnemonic::from_entropy(self.entropy()).unwrap() + Mnemonic::from_entropy(self.entropy()).unwrap() } /// Returns the entropy needed for genearting the BIP-39 mnemonic. fn entropy(&self) -> &[u8] { if self.long { - self.data.as_ref() + self.input.as_ref() } else { - &self.data[..16] + &self.input[..16] } } /// Itearte the hash function repeatedly on the input data. fn hash_iterations(&mut self) { let mut hasher = Sha256::new(); - hasher.update(&self.data); + hasher.update(&self.input); let mut data = hasher.finalize_reset(); for _ in 1..self.iterations { hasher.update(&data); hasher.finalize_into_reset(&mut data); } - self.data = data.to_vec(); - } -} - -impl From for Generator { - fn from(cli: Cli) -> Self { - Generator { - data: cli.get_input(), - iterations: cli.iterations, - long: cli.long, - } + self.input = data.to_vec(); } } @@ -56,7 +44,7 @@ mod tests { pub fn gen12(data: &str) -> Generator { Generator { - data: data.into(), + input: data.into(), iterations: 1, long: false, } @@ -64,7 +52,7 @@ mod tests { pub fn gen24(data: &str) -> Generator { Generator { - data: data.into(), + input: data.into(), iterations: 1, long: true, } diff --git a/src/main.rs b/src/main.rs index 82e1f43..76a69be 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,10 @@ mod cli; mod generator; -mod util; use clap::Parser; use cli::Cli; -use generator::Generator; -fn main() { +fn main() -> anyhow::Result<()> { let cli = Cli::parse(); - let mut gen = Generator::from(cli.clone()); - - let seed = gen.seed(); - cli.write_output(seed.to_string().as_bytes()); + cli.exec() } diff --git a/src/util.rs b/src/util.rs deleted file mode 100644 index 1abf719..0000000 --- a/src/util.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub fn exit_with_error(msg: &str) -> ! { - eprintln!("{msg}"); - std::process::exit(1) -}