├── .gitignore ├── dicts ├── test.txt ├── test_cap.txt ├── test_upper.txt ├── 1k.txt ├── 1k_cap.txt └── 1k_upper.txt ├── .gitmodules ├── Cargo.toml ├── .github └── workflows │ └── rust.yml ├── scripts ├── upload.sh ├── configure_hashcat.sh ├── release.sh └── test_hashcat.sh ├── LICENSE ├── docs ├── benchmarks_8x4090.txt ├── benchmarks_3090.txt ├── renting.md ├── design.md └── recovery.md ├── README.md └── src ├── tests.rs ├── main.rs ├── permutations.rs ├── address.rs ├── logger.rs ├── benchmarks.rs ├── combination.rs ├── passphrase.rs └── hashcat.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .idea 3 | *.iml 4 | *.tmp 5 | *.zip* 6 | -------------------------------------------------------------------------------- /dicts/test.txt: -------------------------------------------------------------------------------- 1 | the 2 | of 3 | and 4 | to 5 | a 6 | in 7 | for 8 | is 9 | on 10 | that 11 | -------------------------------------------------------------------------------- /dicts/test_cap.txt: -------------------------------------------------------------------------------- 1 | The 2 | Of 3 | And 4 | To 5 | A 6 | In 7 | For 8 | Is 9 | On 10 | That 11 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "hashcat"] 2 | path = hashcat 3 | url = https://github.com/seed-cat/hashcat.git 4 | -------------------------------------------------------------------------------- /dicts/test_upper.txt: -------------------------------------------------------------------------------- 1 | THE 2 | OF 3 | AND 4 | TO 5 | A 6 | IN 7 | FOR 8 | IS 9 | ON 10 | THAT 11 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "seedcat" 3 | version = "0.0.3" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | clap = { version = "4.4.7", features = ["derive"] } 8 | anyhow = "1.0" 9 | bitcoin = "0.31" 10 | tokio = { version = "1.33.0", features = ["full"] } 11 | crossterm = "0.27.0" 12 | gzp = {version = "0.11.3", default-features = false, features = ["deflate_rust"] } 13 | sha2 = "0.10.8" -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | steps: 17 | 18 | - uses: actions/checkout@v3 19 | with: 20 | submodules: recursive 21 | 22 | - name: Build 23 | run: cargo build --verbose 24 | 25 | - name: Run tests 26 | run: cargo test --verbose 27 | 28 | - name: Build Submodule 29 | working-directory: ./hashcat 30 | run: make -------------------------------------------------------------------------------- /scripts/upload.sh: -------------------------------------------------------------------------------- 1 | # Uploads the release to a cloud instance and unzips 2 | # Requires ssh support by the server (tested on vast.ai) 3 | 4 | set -e 5 | source ./scripts/configure_hashcat.sh 6 | cd .. 7 | 8 | [ -z $1 ] && echo "You must supply the IP:PORT of the instance" && exit 1 9 | SSH_IP=`echo $1 | cut -d':' -f1` 10 | SSH_PORT=`echo $1 | cut -d':' -f2` 11 | 12 | scp -P $SSH_PORT $PROJECT_NAME.zip root@$SSH_IP:/root/ 13 | ssh -p $SSH_PORT root@$SSH_IP -L 8080:localhost:8080 "sudo apt-get install unzip" 14 | ssh -p $SSH_PORT root@$SSH_IP -L 8080:localhost:8080 "unzip $PROJECT_NAME" 15 | ssh -p $SSH_PORT root@$SSH_IP -L 8080:localhost:8080 16 | -------------------------------------------------------------------------------- /scripts/configure_hashcat.sh: -------------------------------------------------------------------------------- 1 | # Configures the build for hashcat 2 | # We disable unused modules for faster and smaller compilation target 3 | 4 | set -e 5 | CARGO_NAME=$(cargo run -- -V) 6 | export PROJECT_NAME="${CARGO_NAME// /_}" 7 | echo "Running command for $PROJECT_NAME" 8 | 9 | cd hashcat 10 | cd src 11 | MODULES_DISABLE="" 12 | for file in modules/*.c; do 13 | if [ "$file" != "modules/module_02000.c" ] && [ "$file" != "modules/module_00000.c" ] && [ "$file" != "modules/module_28510.c" ]; then 14 | MODULES_DISABLE="$MODULES_DISABLE ${file%.c}.dll ${file%.c}.so" 15 | fi 16 | done 17 | export ENABLE_UNRAR=0 18 | export MODULES_DISABLE 19 | cd .. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 bitcoin-recovery-tool 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | # Builds a new seedcat_*.zip release 2 | # 3 | # Releases are built in a Ubuntu 18.04 Desktop image https://releases.ubuntu.com/18.04/ 4 | # Ensures >2018-02-01 GLIBC compatibility and support until April 2028 5 | # You can run the image in VirtualBox or similar VM software 6 | # Setup your Ubuntu VM for builds with the following commands: 7 | # 8 | # su 9 | # sudo apt update 10 | # sudo apt install gcc-mingw-w64-x86-64 g++-mingw-w64-x86-64 git curl -y 11 | # git clone https://github.com/seed-cat/seedcat 12 | # cd seedcat/ 13 | # git submodule update --init --recursive 14 | # cd .. 15 | # git clone https://github.com/seed-cat/win-iconv.git 16 | # cd win-iconv/ 17 | # sudo make install 18 | # cd ../seedcat 19 | # curl https://sh.rustup.rs -sSf | sh 20 | # source "$HOME/.cargo/env" 21 | # rustup target add x86_64-pc-windows-gnu 22 | # 23 | # ./scripts/release.sh 24 | 25 | SC="seedcat" 26 | HC="$SC/hashcat" 27 | 28 | set -e 29 | source ./scripts/configure_hashcat.sh 30 | 31 | make clean 32 | make binaries 33 | cd .. 34 | cargo build --release --target x86_64-pc-windows-gnu 35 | cargo build --release --target x86_64-unknown-linux-gnu 36 | 37 | cd .. 38 | rm -f "$PROJECT_NAME.zip" 39 | cp "./$SC/target/x86_64-pc-windows-gnu/release/seedcat.exe" $SC 40 | cp "./$SC/target/x86_64-unknown-linux-gnu/release/seedcat" $SC 41 | 42 | zip -r "$PROJECT_NAME.zip" . -i $SC/docs/* $SC/scripts/* $SC/dicts/* $HC/hashcat.exe $HC/hashcat.bin $HC/hashcat.hcstat2 \ 43 | $HC/modules/*.so $HC/modules/*.dll $HC/OpenCL/*.cl $HC/OpenCL/*.h $HC/charsets/* $HC/charsets/*/* 44 | 45 | zip -r "$PROJECT_NAME.zip" -m $SC/seedcat.exe $SC/seedcat 46 | mv "$PROJECT_NAME.zip" $SC -------------------------------------------------------------------------------- /docs/benchmarks_8x4090.txt: -------------------------------------------------------------------------------- 1 | Benchmark Name |Guesses |Speed |GPU Speed |Time |Wall Time 2 | Master XPUB (mask attack) |260M |11.3M/sec |11.3M/sec |23 secs |29 secs 3 | 1000 derivations (mask attack) |251M |4.05M/sec |40.5K/sec |1 mins, 2 secs |1 mins, 10 secs 4 | 10 derivations (mask attack) |260M |8.12M/sec |812K/sec |32 secs |38 secs 5 | 1 derivations (mask attack) |260M |6.05M/sec |6.05M/sec |43 secs |49 secs 6 | Missing first words of 12 |689M |11.3M/sec |706K/sec |1 mins, 1 secs |1 mins, 4 secs 7 | Missing first words of 24 |5.03B |82.5M/sec |322K/sec |1 mins, 1 secs |1 mins, 5 secs 8 | Permute 12 of 12 words |308M |5.05M/sec |316K/sec |1 mins, 1 secs |1 mins, 3 secs 9 | Permute 12 of 24 words |4.45B |72.9M/sec |285K/sec |1 mins, 1 secs |1 mins, 5 secs 10 | Missing last words of 12 |5.69B |93.3M/sec |5.83M/sec |1 mins, 1 secs |1 mins, 5 secs 11 | Missing last words of 24 |94.2B |1.54B/sec |6.03M/sec |1 mins, 1 secs |1 mins, 5 secs 12 | Passphrase dict+dict attack |156M |2.59M/sec |2.59M/sec |1 mins, 0 secs |1 mins, 3 secs 13 | Passphrase dict+mask attack |149M |2.40M/sec |2.40M/sec |1 mins, 2 secs |1 mins, 5 secs 14 | Passphrase mask+dict attack |20.1M |330K/sec |330K/sec |1 mins, 1 secs |1 mins, 5 secs 15 | Small passphrase + seed |4.19B |71.1M/sec |4.44M/sec |59 secs |1 mins, 2 secs 16 | Large passphrase + seed |2.02B |67.2M/sec |4.20M/sec |30 secs |34 secs -------------------------------------------------------------------------------- /docs/benchmarks_3090.txt: -------------------------------------------------------------------------------- 1 | Benchmark Name |Guesses |Speed |GPU Speed |Time |Wall Time 2 | Master XPUB (mask attack) |39.0M |640K/sec |640K/sec |1 mins, 1 secs |1 mins, 3 secs 3 | 1000 derivations (mask attack) |42.0M |677K/sec |6.77K/sec |1 mins, 2 secs |1 mins, 10 secs 4 | 10 derivations (mask attack) |37.8M |619K/sec |61.9K/sec |1 mins, 1 secs |1 mins, 2 secs 5 | 1 derivations (mask attack) |23.0M |376K/sec |376K/sec |1 mins, 1 secs |1 mins, 3 secs 6 | Missing first words of 12 |282M |4.63M/sec |289K/sec |1 mins, 1 secs |1 mins, 2 secs 7 | Missing first words of 24 |3.76B |61.7M/sec |241K/sec |1 mins, 1 secs |1 mins, 3 secs 8 | Permute 12 of 12 words |221M |3.62M/sec |226K/sec |1 mins, 1 secs |1 mins, 1 secs 9 | Permute 12 of 24 words |1.32B |21.7M/sec |84.7K/sec |1 mins, 1 secs |1 mins, 3 secs 10 | Missing last words of 12 |367M |6.02M/sec |376K/sec |1 mins, 1 secs |1 mins, 3 secs 11 | Missing last words of 24 |5.86B |96.0M/sec |375K/sec |1 mins, 1 secs |1 mins, 3 secs 12 | Passphrase dict+dict attack |22.7M |372K/sec |372K/sec |1 mins, 1 secs |1 mins, 2 secs 13 | Passphrase dict+mask attack |22.7M |372K/sec |372K/sec |1 mins, 1 secs |1 mins, 2 secs 14 | Passphrase mask+dict attack |11.5M |189K/sec |189K/sec |1 mins, 1 secs |1 mins, 3 secs 15 | Small passphrase + seed |355M |5.81M/sec |363K/sec |1 mins, 1 secs |1 mins, 3 secs 16 | Large passphrase + seed |364M |5.97M/sec |373K/sec |1 mins, 1 secs |1 mins, 2 secs -------------------------------------------------------------------------------- /docs/renting.md: -------------------------------------------------------------------------------- 1 | # Renting GPUs 2 | Below are the instructions for running `seedcat` on [Vast.ai](https://vast.ai/) on Linux or iOS: 3 | 4 | ### Setup 5 | 1. [Create an account](https://vast.ai/docs/console/introduction) with an email address and password 6 | 2. [Fund the account](https://cloud.vast.ai/billing/) by clicking `Add Credit` (you can pay in bitcoin if you like) 7 | 3. Generate an ssh-key by running the following commands: 8 | ```bash 9 | ssh-keygen -t rsa -v -f "$HOME/.ssh/id_rsa" -N "" 10 | cat $HOME/.ssh/id_rsa.pub 11 | ``` 12 | 13 | This command will print out a long ssh pubkey that looks like this: 14 | ```bash 15 | # Example output from the step above 16 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDu8... name@os 17 | ``` 18 | 19 | 4. [Log into your account](https://cloud.vast.ai/account) then click `ADD SSH KEY` and paste in the key above 20 | 21 | ### Running Recovery 22 | 1. [Choose an instance to create](https://cloud.vast.ai/create/) then click `Change Template` and select `Cuda:12.0.1-Devel-Ubuntu20.04` 23 | 2. We recommend you click `RENT` on a `8x RTX 4090` instance for maximum speed and choose a `datacenter` instance for better security 24 | 3. [Go to your instance](https://cloud.vast.ai/instances/) and click on `Connect` and copy the `Direct ssh connect` 25 | 4. Paste the command into your terminal, it will look something like this: 26 | ```bash 27 | # Example command from the step above 28 | ssh -p 19879 root@140.228.20.3 -L 8080:localhost:8080 29 | ``` 30 | 5. Once your terminal changes to something like `root@C.14891369:~$` to indicate you are logged into the remote instance, then paste in the following commands: 31 | ```bash 32 | # Change this to the latest version if you like 33 | VERSION=0.0.2 34 | 35 | # Get the seedcat binaries 36 | wget https://github.com/seed-cat/seedcat/releases/download/v$VERSION/seedcat_$VERSION.zip 37 | wget https://github.com/seed-cat/seedcat/releases/download/v$VERSION/seedcat_$VERSION.zip.sig 38 | sudo apt-get install unzip pgp -y 39 | ``` 40 | 41 | 6. Verify the signatures and unzip the seedcat binaries like so: 42 | ``` 43 | # Verify signatures and run seedcat 44 | gpg --keyserver keyserver.ubuntu.com --recv-keys D249C16D6624F2C1DD0AC20B7E1F90D33230660A 45 | gpg --verify seedcat_$VERSION.zip.sig || exit 46 | 47 | unzip seedcat_$VERSION.zip 48 | cd seedcat 49 | ./seedcat 50 | ``` 51 | 52 | For instructions on how to use seedcat [see the documentation](recovery.md) 53 | -------------------------------------------------------------------------------- /scripts/test_hashcat.sh: -------------------------------------------------------------------------------- 1 | # Tests for when working on the hashcat module 2 | 3 | set -e 4 | source ./scripts/configure_hashcat.sh 5 | 6 | if [ -z $1 ]; then 7 | echo "Usage ./scripts/test_hashcat.sh <#|all> [clean]" 8 | echo " 'test' executes one hash (good for debugging)" 9 | echo " 'run' executes multiple hashes" 10 | echo " 'bench' executes for a long time without finding a solution" 11 | echo " 'build' executes the make command and terminates" 12 | exit 1 13 | fi 14 | 15 | rm -f modules/module_28510.so 16 | rm -f kernels/* 17 | rm -f hashcat.potfile 18 | 19 | if [[ "$3" = "clean" ]]; then 20 | make clean 21 | fi 22 | make 23 | 24 | if [[ "$1" = "build" ]]; then 25 | exit 0 26 | fi 27 | 28 | TEST1="XPUB:m/:coral,dice,harvest:xpub661MyMwAqRbcFaizXLqdLrqBkUJo4JyyXYNucU2hWQBDfhmCd3TL7USdpjhUddedvEiSo31BRg9QB4a5PNKcuQRWT6DA2YveGA2tzsqZQwg" 29 | PASS1="hashca?4" 30 | EXPECTED1="xpub661MyMwAqRbcFaizXLqdLrqBkUJo4JyyXYNucU2hWQBDfhmCd3TL7USdpjhUddedvEiSo31BRg9QB4a5PNKcuQRWT6DA2YveGA2tzsqZQwg:hashcat" 31 | 32 | TEST2="P2PKH:m/0/0,m/44h/0h/0h/0/0:balcony,catalog,winner,letter,alley,this:1NSHnSJpJPWXgMAXk88ktQPTFyniPeUaod" 33 | PASS2="hashca?4" 34 | EXPECTED2="1NSHnSJpJPWXgMAXk88ktQPTFyniPeUaod:hashcat" 35 | 36 | TEST3="P2SH-P2WPKH:m/49h/0h/0h/0/1:cage,keep,stone,swarm,open,race,toward,state,subway,dutch,extra,short,purpose,interest,enough,idle,found,guilt,will,salt,mixed,boil,heavy,thing:361yU4TkuRSLTdTkfEUbWGfTJgJjFDZUvG" 37 | PASS3="hashca?4" 38 | EXPECTED3="361yU4TkuRSLTdTkfEUbWGfTJgJjFDZUvG:hashcat" 39 | 40 | TEST4="P2WPKH:m/84h/0h/0h/0/2:donate,dolphin,bachelor,excess,stuff,flower,spread,crazy,scorpion,zoo,skull,lottery:bc1q490ra0dcf4l58jzt2445akrxpj6aftkfdvs8n7" 41 | PASS4="hashca?4" 42 | EXPECTED4="bc1q490ra0dcf4l58jzt2445akrxpj6aftkfdvs8n7:hashcat" 43 | 44 | # security sugar abandon diamond abandon orient zoo example crane fruit senior decade 45 | # '=' means include in the result 46 | TEST5="P2WPKH:m/84h/0h/0h/0/0:=security,sugar,?,=diamond,?,orient,?,example,crane,fruit,senior,?:bc1q6dlx8mxcxm3qterx35cul7z76v975tf2vq06yr" 47 | PASS5="5656?1?2?3hashcat" 48 | EXPECTED5="bc1q6dlx8mxcxm3qterx35cul7z76v975tf2vq06yr:security,abandon,diamond,abandon,zoo,decade,hashcat" 49 | 50 | # can also use numeric indexes (faster and more compact to parse) 51 | TEST6="P2WPKH:m/84h/0h/0h/0/0:1558,1734,0,489,0,1252,2047,627,402,750,1565,?:bc1q6dlx8mxcxm3qterx35cul7z76v975tf2vq06yr" 52 | PASS6="?3hashcat" 53 | EXPECTED6="bc1q6dlx8mxcxm3qterx35cul7z76v975tf2vq06yr:decade,hashcat" 54 | 55 | # m/84'/0'/4'/0/5 56 | TEST7="P2WPKH:m/84h/0h/?5h/0/?5:paper,warrior,title,join,assume,trumpet,setup,angle,helmet,salmon,save,love:bc1qhatcz9ljzuucd6en9sr3p9mlt7t78654h9hqf6" 57 | PASS7="hashca?4" 58 | EXPECTED7="bc1qhatcz9ljzuucd6en9sr3p9mlt7t78654h9hqf6:hashcat" 59 | 60 | TEST8="P2PKH:m/0/0,m/44h/0h/0h/0/0:security,sugar,abandon,diamond,abandon,orient,?,example,crane,fruit,senior,?:1HXGvcN88JpBPAFhd1CmjMKcbbM8H2a9TP" 61 | PASS8="?1?2?3" 62 | EXPECTED8="1HXGvcN88JpBPAFhd1CmjMKcbbM8H2a9TP:zoo,decade," 63 | 64 | TOTAL=8 65 | 66 | for i in $(seq 1 $TOTAL); 67 | do 68 | if [[ "$2" = "$i" ]] || [[ "$2" = "all" ]]; then 69 | echo "Running $1 #$i" 70 | TEST="TEST$i" 71 | PASS="PASS$i" 72 | EXPECTED="EXPECTED$i" 73 | if [ $1 = "test" ]; then 74 | ./hashcat -m 28510 -a 3 --self-test-disable --force -n 1 -u 1 -T 1 -1 T -2 u -3 S -4 t "${!TEST}" "${!PASS}" 75 | elif [ $1 = "run" ]; then 76 | ./hashcat -m 28510 -a 3 --self-test-disable -1 charsets/bin/5bit.hcchr -2 charsets/bin/6bit.hcchr -3 charsets/bin/7bit.hcchr -4 ?l "${!TEST}" "${!PASS}" 77 | elif [ $1 = "bench" ]; then 78 | ./hashcat -m 28510 --self-test-disable -a 3 -1 charsets/bin/5bit.hcchr -2 charsets/bin/6bit.hcchr -3 charsets/bin/7bit.hcchr -4 ?l --status "${!TEST}" "?1?2?1?2?1?2?3?l" 79 | fi 80 | 81 | # Validate results 82 | RESULT=$( tail -n 1 hashcat.potfile ) 83 | if [[ $RESULT = "${!EXPECTED}" ]]; then 84 | echo -e "\n========== Test $i Passed ==========" 85 | else 86 | echo -e "\n========== Test $i Failed ==========" 87 | echo "RESULT: $RESULT" 88 | echo "EXPECTED: ${!EXPECTED}" 89 | exit 1 90 | fi 91 | fi 92 | done 93 | 94 | echo -e "\n========== All Results ==========" 95 | cat hashcat.potfile 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # seedcat 2 | The [world's fastest](docs/design.md#benchmarks) bitcoin seed phrase recovery tool. 3 | 4 | Modern bitcoin wallets use [BIP39 seed phrases](https://en.bitcoin.it/wiki/Seed_phrase) which consist of 12 or 24 seed words and an optional passphrase. 5 | 6 | Users who lose part of their seed phrase cannot access their bitcoin. 7 | For instance, seed phrase backups might be incorrectly transcribed or damaged by natural disasters. 8 | Memorized seed phrases may be partially forgotten or lost due to death. 9 | 10 | `seedcat` helps you recover missing parts of your seed phrase: 11 | - Guesses seed words that are missing, fragmented, or out-of-order 12 | - Guesses passphrases using dictionaries and/or wildcards 13 | - Leverages GPU parallelism to guess millions of seed phrases per second 14 | - Open-source, non-custodial software that can run on your own computer 15 | 16 | No need to trust third-party recovery services who charge up to 20% of your funds. 17 | 18 | ## Recovery instructions 19 | `seedcat` attempts to guess seed phrases using your GPU 20 | 21 | 1. For NVIDIA GPUs install [CUDA](https://developer.nvidia.com/cuda-downloads) (other platforms see [hashcat documentation](https://hashcat.net/hashcat/)) 22 | 2. Download the [latest release zip](https://github.com/seed-cat/seedcat/releases) and extract the folder 23 | 3. Optionally you can verify the release like so: 24 | ``` 25 | > gpg --keyserver keyserver.ubuntu.com --recv-keys D249C16D6624F2C1DD0AC20B7E1F90D33230660A 26 | > gpg --verify seedcat_*.zip.sig 27 | 28 | gpg: Good signature from "Seed Cat " [unknown] 29 | Primary key fingerprint: D249 C16D 6624 F2C1 DD0A C20B 7E1F 90D3 3230 660A 30 | ``` 31 | 32 | 4. Run `seedcat` on Linux or `seedcat.exe` on Windows to view the command-line options. 33 | 5. See our [recovery examples](docs/recovery.md) for detailed instructions. 34 | 35 | If you have issues running locally or need larger GPU clusters see [renting in the cloud documentation](docs/renting.md) 36 | 37 | ## Security concerns 38 | Since `seedcat` handles your seed phrase you should take the following security precautions: 39 | - Disable your internet access before running with any real seed phrase information 40 | - Sweep all bitcoin to a new wallet before enabling internet access 41 | - For large recoveries it is safer to build your own GPU cluster than rent 42 | 43 | Also note that: 44 | - All code is open-source so anyone can verify there is no malicious code 45 | - You can build from source by following the steps in [./scripts/release.sh](./scripts/release.sh) 46 | 47 | ## Performance issues 48 | If recovery is taking too long first try to reduce the number of guesses: 49 | - `XPUB` offers ~2x the speed and works on non-standard derivation paths and scripts 50 | - Otherwise try to specify the exact derivation path for your address 51 | - When seed word guessing specify letters to reduce the possible words (e.g. `so?` instead of `s?`) 52 | - Leaving the last seed word as `?` may run faster on some systems (by allowing for pure GPU mode) 53 | - When seed word descrambling anchor words with `^` to reduce the permutations 54 | - For passphrase mask attacks use the most restrictive wildcards (e.g. `?l` instead of `?a`) or custom charsets 55 | - For passphrase dictionary attacks try the most frequent words first 56 | 57 | You may also need to upgrade your hardware: 58 | - You should see all your GPUs print out when running 59 | - A high-end gaming computer can handle ~100B guesses within a day 60 | - An 8+ GPU cluster can handle ~1T guesses within a day 61 | - You can test out your recovery speed in the [cloud](docs/renting.md) (using a dummy seed phrase) 62 | 63 | ## Contributing 64 | All contributions are welcome, including reporting bugs or missing features through [new issues](https://github.com/seed-cat/seedcat/issues). 65 | 66 | Check out our high-level [design docs](docs/design.md). 67 | 68 | Developers will need to be able to compile Rust and C code. You can setup your machine like so: 69 | 70 | ```bash 71 | sudo apt update 72 | sudo apt install gcc-mingw-w64-x86-64 g++-mingw-w64-x86-64 make git curl 73 | curl https://sh.rustup.rs -sSf | sh 74 | 75 | git clone git@github.com:seed-cat/seedcat.git 76 | cd seedcat 77 | git submodule update --init --recursive 78 | ``` 79 | 80 | For the Rust unit tests run `cargo test` 81 | 82 | For the C unit tests (if modifying the hashcat code) run `./scripts/test_hashcat.sh test all` 83 | 84 | For the integration tests run `cargo run -r -- test -t` 85 | 86 | ## Reach out 87 | If you have more questions you can contact seedcat through the following: 88 | - [Email: seedcat@protonmail.com](mailto:seedcat@protonmail.com) 89 | - [Open a Github Issue](https://github.com/seed-cat/seedcat/issues/new) 90 | -------------------------------------------------------------------------------- /docs/design.md: -------------------------------------------------------------------------------- 1 | # Seedcat design 2 | Bitcoin users are switching from [insecure brain wallets](https://fc16.ifca.ai/preproceedings/36_Vasek.pdf) to modern wallets that implement [BIP39](https://en.bitcoin.it/wiki/BIP_0039) seed phrases. 3 | 4 | Over the next decade these wallets will likely predominate bitcoin usage. 5 | 6 | Yet no optimized recovery tool existed for recovering these wallets. 7 | 8 | `seedcat` was designed to be the fastest modern wallet recovery tool through two components: 9 | - A backend in C that leverages the GPU-optimized algorithms from being a [hashcat](https://hashcat.net/wiki/) plugin 10 | - A frontend CLI in Rust that simplifies the recovery for users and generates valid seeds in parallel 11 | 12 | # Hashcat backend 13 | The most expensive part of generating private keys from seed phrases is 2048 iterations of the PBKDF2-HMAC-SHA512 algorithm. Luckily we can easily run this algorithm in parallel on GPUs and hashcat already has the fastest implementation. 14 | 15 | If the user has the master XPUB we already have 128-bits of entropy we can check our hash against. Otherwise we need to perform some ECC operations that are also optimized in hashcat. 16 | 17 | The most complicated aspect of the plugin is the guessing of seed words because we need to take advantage of the [BIP-39 checksum](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#generating-the-mnemonic). With 12 seed words we can filter for 1/16 the seed phrases and with 24 seed words we can filter for 1/256 the seed phrases, translating to a 10-100x improvement in speed. 18 | 19 | Unfortunately GPUs do not perform well with branching code that would be required to filter seeds so we perform filtering on the CPU and send it to the hashcat module running on the GPU through stdin or the hashes file. 20 | 21 | # Seedcat frontend 22 | The frontend determines the fastest way we can run the recovery. There are 3 modes that `seedcat` can run in: 23 | - **Pure GPU** - if performing a passphrase attack with <10M valid seeds we pregenerate all valid seeds and put them in the hashes file 24 | - **Binary charsets** - if the last word is `?` then we can pass in the seed entropy directly and no seed filtering is required since we can quickly generate the checksum on the GPU 25 | - **Stdin Mode** - if we are generating many seeds and the last word is constrained then we filter seeds on parallel in the frontend and send them to the GPU module via stdin in. In this mode we end up CPU-bound if running on a large GPU cluster so we try to avoid it when possible. 26 | 27 | Generating and filtering valid seeds also needs to be multithreaded and fast so we wrote highly-optimized Rust code that allows us to parallelize the work. In order to split the work across threads we perform some tricks such as using lexicographic seed word permutations that allow us to split a large permutation in O(1) time. 28 | 29 | The rest of the frontend is dedicated to providing a more user-friendly UX. For instance we validate user inputs, provide total counts, and examples so a user can understand what is actually being guessed. 30 | 31 | # Benchmarks 32 | There have been only two other GPU-optimized BIP39 seed phrase recovery tools written: [btcrecover](https://github.com/gurnec/btcrecover) and John Cantrell's [one-off implementation](https://medium.com/@johncantrell97/how-i-checked-over-1-trillion-mnemonics-in-30-hours-to-win-a-bitcoin-635fe051a752). 33 | 34 | Since both implementations have fallen out of maintenance `seedcat` provides superior features and support. 35 | In fact we had trouble getting either implementation running with CUDA so we had to rely on their self-reported benchmarks and ran comparisons on similar hardware (A2000 for btcrecover and 2080 TI for Cantrell). 36 | 37 | | Test | Implementation | Relative Speed | 38 | |---------------|----------------|----------------| 39 | | 12 seed words | btcrecover | 1.0x | 40 | | 12 seed words | johncantrell97 | 4.3x | 41 | | 12 seed words | **seedcat** | **9.7x** | 42 | | 24 seed words | btcrecover | 1.0x | 43 | | 24 seed words | **seedcat** | **103.9x** | 44 | | Passphrase | btcrecover | 1.0x | 45 | | Passphrase | **seedcat** | **71.3x** | 46 | 47 | We can see that `seedcat` offers around a **10-100x** improvement in speed. The improvement against btcrecover is likely because it isn't filtering invalid checksums in a fast way and its passphrase attack isn't GPU-optimized. 48 | 49 | Against the johncantrell97 implementation the speed-up is smaller and occurs due to better GPU optimizations. Note that if we attack the master XPUB instead of the address we could gain an additional 2x speed-up. His implementation was also written as a one-off to win a contest so doesn't support passphrases or any other kinds of attack. 50 | 51 | In comparisons against CPU-only recovery tools we generally see a **>50x** improvement in speed which easily gets much higher if running on powerful GPU clusters. On a 8x 4090 RTX cluster we measured a **~450x** improvement over a CPU-based implementation. -------------------------------------------------------------------------------- /src/tests.rs: -------------------------------------------------------------------------------- 1 | use std::sync::atomic::{AtomicUsize, Ordering}; 2 | 3 | use anyhow::{bail, Result}; 4 | use clap::Parser; 5 | use crossterm::style::Stylize; 6 | 7 | use crate::hashcat::{Hashcat, HashcatRunner}; 8 | use crate::logger::Logger; 9 | use crate::seed::Finished; 10 | use crate::{configure, Cli}; 11 | 12 | static TEST_COUNT: AtomicUsize = AtomicUsize::new(0); 13 | 14 | pub struct Test { 15 | args: String, 16 | expected: Finished, 17 | binary: bool, 18 | } 19 | 20 | impl Test { 21 | pub fn configure(prefix: &str, args: &str, log: &Logger) -> Hashcat { 22 | let mut args: Vec<_> = args.split(" ").into_iter().collect(); 23 | args.insert(0, ""); 24 | args.push("-y"); 25 | args.push("--"); 26 | 27 | let cli = Cli::parse_from(args).run.unwrap(); 28 | let mut hashcat = configure(&cli, &log).unwrap(); 29 | hashcat.set_prefix(prefix.to_string()); 30 | hashcat 31 | } 32 | 33 | pub async fn run(&self) -> Result<()> { 34 | let id = TEST_COUNT.fetch_add(1, Ordering::Relaxed); 35 | let name = format!("hc_test{}", id); 36 | 37 | let log = Logger::new(); 38 | let mut hashcat = Self::configure(&name, &self.args, &log); 39 | if self.expected.pure_gpu { 40 | hashcat.min_passphrases = 0; 41 | } else { 42 | hashcat.max_hashes = 0; 43 | } 44 | 45 | if self.binary { 46 | if !matches!( 47 | hashcat.get_mode().unwrap().runner, 48 | HashcatRunner::BinaryCharsets(_, _) 49 | ) { 50 | bail!("Expected binary mode for test '{}'", name); 51 | } 52 | } 53 | 54 | let run = hashcat.run(&log, false); 55 | let (_, result) = run.await.unwrap(); 56 | if result != self.expected { 57 | bail!("{} Failed: {}\nExpected: {}", name, result, self.expected); 58 | } else { 59 | Ok(()) 60 | } 61 | } 62 | } 63 | 64 | struct Tests { 65 | tests: Vec, 66 | } 67 | 68 | impl Tests { 69 | fn new() -> Self { 70 | Self { tests: vec![] } 71 | } 72 | 73 | fn test_both(&mut self, args: &str, expected: &str) { 74 | self.test(args, expected, false, false); 75 | self.test(args, expected, true, false); 76 | } 77 | 78 | fn test_stdin(&mut self, args: &str, expected: &str) { 79 | self.test(args, expected, false, false); 80 | } 81 | 82 | fn test_binary(&mut self, args: &str, expected: &str) { 83 | self.test(args, expected, true, true); 84 | } 85 | 86 | fn test(&mut self, args: &str, expected: &str, pure_gpu: bool, binary: bool) { 87 | let expected: Vec<_> = expected.split(" ").collect(); 88 | self.tests.push(Test { 89 | args: args.to_string(), 90 | expected: Finished::new(expected[0], expected.get(1).unwrap_or(&""), pure_gpu), 91 | binary, 92 | }) 93 | } 94 | } 95 | 96 | pub async fn run_tests() -> Result<()> { 97 | let mut tests = Tests::new(); 98 | 99 | tests.test_binary("-a 1Mbe4MHF4awqg2cojz8LRJErKaKyoQjsiD -s harbor,?,clinic,index,mix,shoe,tube,awkward,food,acquire,sustain,?", 100 | "harbor,acquire,clinic,index,mix,shoe,tube,awkward,food,acquire,sustain,pumpkin"); 101 | 102 | tests.test_binary("-a 1HJVf7UhgHKhvMyKVuQMhzrvGQ9QSGUARQ -s harbor,a?,clinic,index,mix,shoe,tube,awkward,food,acquire,sustain,? -p hashcat", 103 | "harbor,acquire,clinic,index,mix,shoe,tube,awkward,food,acquire,sustain,pumpkin hashcat"); 104 | 105 | tests.test_binary("-a 1HJVf7UhgHKhvMyKVuQMhzrvGQ9QSGUARQ -s harbor,acquire,clinic,index,mix,shoe,tube,awkward,food,acquire,sustain,? -p hashca?l", 106 | "harbor,acquire,clinic,index,mix,shoe,tube,awkward,food,acquire,sustain,pumpkin hashcat"); 107 | 108 | tests.test_stdin("-a 1CFizqjfv4kGz4PbvMviXY84Z73D7PSdR1 -s zoo,survey,thought,^hill,^friend,^fatal,^fall,^amused,^pact,^ripple,^glance,^rural,hand -c 12", 109 | "hand,thought,survey,hill,friend,fatal,fall,amused,pact,ripple,glance,rural"); 110 | 111 | tests.test_stdin("-a 1Hh5BipqjUyFJXXynux6ReTdEN5vStpQvn -s ?,r?,weather,dish,swall?|zoo,water,mosquito,merry,icon,congress,blush,section", 112 | "there,river,weather,dish,swallow,water,mosquito,merry,icon,congress,blush,section"); 113 | 114 | tests.test_stdin("-a 39zQn8yBDmUHswRYUgjwwEe6y5b6wDTUTi -s skill,check,filter,camera,pond,oppose,lesson,delay,rare,prepare,oak,bring,tape,fancy,pulp,voyage,coil,spot,faculty,nominee,rough,stick,?,enter", 115 | "skill,check,filter,camera,pond,oppose,lesson,delay,rare,prepare,oak,bring,tape,fancy,pulp,voyage,coil,spot,faculty,nominee,rough,stick,wide,enter"); 116 | 117 | tests.test_stdin("-a bc1qscpdw0smafzpwe5s9kjfstq48p6vcz0n30sccs -s p?,stumble,print,mansion,occur,client,deposit,electric,dance,olive,stay,mom -d m/0/0/?99,m/84'/0'/?2'/0/?3", 118 | "private,stumble,print,mansion,occur,client,deposit,electric,dance,olive,stay,mom"); 119 | 120 | tests.test_binary("-a bc1qscpdw0smafzpwe5s9kjfstq48p6vcz0n30sccs -s private,stumble,print,mansion,occur,client,deposit,electric,dance,olive,stay,? -d m/0/0/?99|m/84'/0'/?2'/0/?3", 121 | "private,stumble,print,mansion,occur,client,deposit,electric,dance,olive,stay,mom"); 122 | 123 | tests.test_both("-a xpub661MyMwAqRbcF5snxLXxdet4WwyipbK6phjJdy5ViauCkTSjQc37zm6Gyyryq1aF8Uuj4Xub9Bh7LfQo8ZmNujZVczj1FVs1wMDWrnTym39 -s very,cart,matter,object,raise,predict,water,term,easy,play,?,earn -p hashca?2 -2 zt", 124 | "very,cart,matter,object,raise,predict,water,term,easy,play,give,earn hashcat"); 125 | 126 | tests.test_both("-a 1AeC6MA7U651BTVS5hWTGi5u9Z7tGtkE6y -s very,cart,matter,object,raise,predict,water,term,easy,play,give,earn -p ./dicts/test.txt,-,./dicts/test_cap.txt,- -p ./dicts/test.txt", 127 | "very,cart,matter,object,raise,predict,water,term,easy,play,give,earn the-Of-and"); 128 | 129 | tests.test_both("-a 1Gmu1iEtjmnhrB8svoFDiFjYsc4sqXuU7z -s very,cart,matter,object,raise,predict,water,term,easy,play,give,earn -p ./dicts/test.txt,-- -p ?d", 130 | "very,cart,matter,object,raise,predict,water,term,easy,play,give,earn and--2"); 131 | 132 | tests.test_both("-a 1Hv3dB4JyhDBwo1vDzPKKJZp4SpxaoES6L -s very,cart,matter,object,raise,predict,water,term,easy,play,give,earn -p ?d?d-- -p ./dicts/test.txt", 133 | "very,cart,matter,object,raise,predict,water,term,easy,play,give,earn 12--the"); 134 | 135 | tests.test_both("-a 18zpD3jMSrHAYoA1XcDLshXPJA46DocVNi -s very,cart,matter,object,raise,predict,water,term,easy,play,give,earn -p ?dmask?d", 136 | "very,cart,matter,object,raise,predict,water,term,easy,play,give,earn 1mask2"); 137 | 138 | let num = tests.tests.len(); 139 | let mut passed = 0; 140 | for test in tests.tests.drain(..) { 141 | test.run().await?; 142 | passed += 1; 143 | let output = format!("{}/{} tests passed.", passed, num); 144 | println!("{}", output.as_str().dark_green()); 145 | } 146 | Ok(()) 147 | } 148 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::io::{BufRead, Write}; 2 | use std::net::{SocketAddr, TcpStream}; 3 | use std::path::Path; 4 | use std::process::exit; 5 | use std::str::FromStr; 6 | use std::time::Duration; 7 | use std::{env, io}; 8 | 9 | use anyhow::{bail, Result}; 10 | use clap::{Args, Parser, Subcommand}; 11 | use crossterm::style::Stylize; 12 | 13 | use crate::address::AddressValid; 14 | use crate::benchmarks::run_benchmarks; 15 | use crate::hashcat::{Hashcat, HashcatExe, HashcatRunner}; 16 | use crate::logger::Logger; 17 | use crate::passphrase::Passphrase; 18 | use crate::seed::{Finished, Seed}; 19 | 20 | mod address; 21 | mod benchmarks; 22 | mod combination; 23 | mod hashcat; 24 | mod logger; 25 | mod passphrase; 26 | mod permutations; 27 | mod seed; 28 | mod tests; 29 | 30 | const HASHCAT_PATH: &str = "hashcat"; 31 | const SEPARATOR: &str = ","; 32 | 33 | #[derive(Parser, Debug)] 34 | #[command(author, version, about, long_about = None, arg_required_else_help = true, args_conflicts_with_subcommands = true)] 35 | pub struct Cli { 36 | #[command(subcommand)] 37 | pub cmd: Option, 38 | 39 | #[command(flatten)] 40 | pub run: Option, 41 | } 42 | 43 | #[derive(Subcommand, Debug)] 44 | pub enum CliCommand { 45 | /// Runs benchmarks and tests of the application 46 | Test(BenchOption), 47 | } 48 | 49 | #[derive(Args, Debug)] 50 | #[group(required = true, multiple = true)] 51 | pub struct BenchOption { 52 | /// Runs all checks for a release (equivalent to -t -b -p -d) 53 | #[arg(short = 'r', long, default_value_t = false)] 54 | release: bool, 55 | 56 | /// Runs integration tests 57 | #[arg(short = 't', long, default_value_t = false)] 58 | test: bool, 59 | 60 | /// Checks whether benchmarks are passing 61 | #[arg(short = 'p', long, default_value_t = false)] 62 | pass: bool, 63 | 64 | /// Runs benchmarks until exhaustion 65 | #[arg(short = 'b', long, default_value_t = false)] 66 | bench: bool, 67 | 68 | /// Diffs the output against benchmarks_.txt file 69 | #[arg(short = 'd', long, value_name = "suffix")] 70 | diff: Option, 71 | } 72 | 73 | #[derive(Args, Debug)] 74 | pub struct CliRun { 75 | /// Address e.g. 'bc1q490...' OR master xpub key e.g. 'xpub661MyMwAqRbc...' 76 | #[arg(short, long, value_name = "address")] 77 | address: String, 78 | 79 | /// Seed words with wildcards e.g. 'cage,?,zo?,?be,?oo?,toward|st?,able...' 80 | #[arg(short, long, value_name = "word word...")] 81 | seed: String, 82 | 83 | /// Derivation paths with wildcards e.g. 'm/0/0,m/49h/0h/0h/?2/?10' 84 | #[arg(short, long, value_name = "path path...")] 85 | derivation: Option, 86 | 87 | /// Dictionaries and/or mask e.g. './dict.txt' '?l?l?l?d?1' 88 | #[arg(short, long, value_name = "MASK|DICT")] 89 | passphrase: Option>, 90 | 91 | /// Guess all permutations of a # of seed words 92 | #[arg(short, long, value_name = "# words")] 93 | combinations: Option, 94 | 95 | /// User defined charset for use in passphrase mask attack 96 | #[arg(short = '1', long, value_name = "chars")] 97 | custom_charset1: Option, 98 | 99 | /// User defined charset for use in passphrase mask attack 100 | #[arg(short = '2', long, value_name = "chars")] 101 | custom_charset2: Option, 102 | 103 | /// User defined charset for use in passphrase mask attack 104 | #[arg(short = '3', long, value_name = "chars")] 105 | custom_charset3: Option, 106 | 107 | /// User defined charset for use in passphrase mask attack 108 | #[arg(short = '4', long, value_name = "chars")] 109 | custom_charset4: Option, 110 | 111 | /// Skips the prompt and starts immediately 112 | #[arg(short = 'y', long, default_value_t = false)] 113 | skip_prompt: bool, 114 | 115 | /// Pass options directly to hashcat (https://hashcat.net/wiki/doku.php?id=hashcat) 116 | #[arg(last = true, value_name = "hashcat options")] 117 | hashcat: Vec, 118 | } 119 | 120 | #[tokio::main(flavor = "multi_thread")] 121 | async fn main() { 122 | let log = Logger::new(); 123 | 124 | let cli: Cli = Cli::parse(); 125 | if let Some(CliCommand::Test(option)) = cli.cmd { 126 | if let Err(err) = run_benchmarks(option).await { 127 | log.println_err(&err.to_string()); 128 | exit(1); 129 | } 130 | exit(0); 131 | } 132 | 133 | if let Some(run) = cli.run { 134 | let mut hashcat = match configure(&run, &log) { 135 | Ok(hashcat) => hashcat, 136 | Err(err) => return log.println_err(&err.to_string()), 137 | }; 138 | let (_, finished) = match hashcat.run(&log, false).await { 139 | Ok(finished) => finished, 140 | Err(err) => return log.println_err(&err.to_string()), 141 | }; 142 | log_finished(&finished, &log); 143 | } 144 | } 145 | 146 | pub fn log_finished(finished: &Finished, log: &Logger) { 147 | match finished { 148 | Finished { 149 | seed: Some(seed), 150 | passphrase: Some(passphrase), 151 | .. 152 | } => { 153 | log.print("Found Seed: ".dark_green().bold()); 154 | log.println(seed.as_str().stylize()); 155 | if !passphrase.is_empty() { 156 | log.print("Found Passphrase: ".dark_green().bold()); 157 | log.println(passphrase.as_str().stylize()); 158 | } 159 | } 160 | _ => log.println_err("Exhausted search with no results...try with different parameters"), 161 | } 162 | log.println("".stylize()); 163 | } 164 | 165 | pub fn configure(cli: &CliRun, log: &Logger) -> Result { 166 | let exe = validate_exe()?; 167 | 168 | let seed_arg = cli.seed.clone(); 169 | let seed = Seed::from_args(&seed_arg, &cli.combinations)?; 170 | seed.validate_length()?; 171 | 172 | let address = AddressValid::from_arg(&cli.address, &cli.derivation)?; 173 | 174 | let passphrase = match &cli.passphrase { 175 | None => None, 176 | Some(args) => { 177 | let charsets = vec![ 178 | cli.custom_charset1.clone(), 179 | cli.custom_charset2.clone(), 180 | cli.custom_charset3.clone(), 181 | cli.custom_charset4.clone(), 182 | ]; 183 | Some(Passphrase::from_arg(args, &charsets)?) 184 | } 185 | }; 186 | 187 | log.heading("Seedcat Configuration"); 188 | let format_address = format!("{} ({}) Address: ", address.kind.key, address.kind.name); 189 | log.print(format_address.as_str().bold()); 190 | log.println(format!("{}\n", address.formatted).as_str().stylize()); 191 | log.format_attempt("Derivations", &address.derivations); 192 | log.format_attempt("Seeds", &seed); 193 | if let Some(passphrase) = &passphrase { 194 | log.format_attempt("Passphrases", passphrase); 195 | } 196 | 197 | if seed.valid_seeds() == 0 { 198 | bail!("All possible seeds have invalid checksums") 199 | } 200 | let args = cli.hashcat.clone(); 201 | let hashcat = Hashcat::new(exe, address.clone(), seed, passphrase, args); 202 | 203 | if hashcat.total() == u64::MAX { 204 | bail!("Exceeding 2^64 attempts will take forever to run, try reducing combinations"); 205 | } 206 | log.print_num("Total Guesses: ", hashcat.total()); 207 | 208 | let mode = hashcat.get_mode()?; 209 | match mode.runner { 210 | HashcatRunner::PureGpu => { 211 | log.print(" Pure GPU Mode: Can run on large GPU clusters\n".stylize()) 212 | } 213 | HashcatRunner::BinaryCharsets(_, _) => log.print( 214 | " Pure GPU Mode: Can run on large GPU clusters (using binary charsets)\n".stylize(), 215 | ), 216 | HashcatRunner::StdinMaxHashes => log.print( 217 | " Stdin Mode: CPU-limited due to a large number of seeds to guess\n".dark_yellow(), 218 | ), 219 | HashcatRunner::StdinMinPassphrases => log.print( 220 | " Stdin Mode: CPU-limited due to not enough passphrases to guess\n".dark_yellow(), 221 | ), 222 | } 223 | if has_internet() { 224 | log.println( 225 | " Warning: For better security turn off your internet connection".dark_yellow(), 226 | ); 227 | } 228 | 229 | if !cli.skip_prompt { 230 | prompt_continue(log); 231 | } 232 | 233 | log.heading("Seedcat Recovery"); 234 | 235 | Ok(hashcat) 236 | } 237 | 238 | fn has_internet() -> bool { 239 | // See if we can connect to Google 240 | let socket = SocketAddr::from_str("209.85.233.101:80").expect("Valid socket"); 241 | TcpStream::connect_timeout(&socket, Duration::from_millis(100)).is_ok() 242 | } 243 | 244 | fn prompt_continue(log: &Logger) { 245 | log.print("\nContinue with recovery [Y/n]? ".stylize()); 246 | io::stdout().flush().unwrap(); 247 | let mut line = String::new(); 248 | let stdin = io::stdin(); 249 | stdin.lock().read_line(&mut line).unwrap(); 250 | if line.contains("n") { 251 | exit(0); 252 | } 253 | } 254 | 255 | fn validate_exe() -> Result { 256 | let platform = match env::consts::FAMILY { 257 | "unix" => "hashcat.bin", 258 | "windows" => "hashcat.exe", 259 | p => bail!("Unable to identify '{}' family", p), 260 | }; 261 | 262 | let hashcat = Path::new(HASHCAT_PATH); 263 | for executable in [platform, "hashcat"] { 264 | if hashcat.join(executable).exists() { 265 | if let Ok(exe) = std::fs::canonicalize(hashcat.join(executable)) { 266 | return Ok(HashcatExe::new(exe)); 267 | } 268 | } 269 | } 270 | bail!( 271 | "Could not find executable '{}'...make sure you are running in 'seedcat' directory", 272 | hashcat.join(platform).to_str().unwrap(), 273 | ); 274 | } 275 | -------------------------------------------------------------------------------- /docs/recovery.md: -------------------------------------------------------------------------------- 1 | # Seed Recovery 2 | Any recovery requires a [bitcoin address](https://en.bitcoin.it/wiki/Invoice_address) and some seed words from your wallet. 3 | 4 | For example if you memorized your seed words but can't remember the first 3 words then you could use the `?` wildcard: 5 | ```bash 6 | seedcat --address "1AtD3g5AmR4fMsCRa1haNGmvCTVWq7YfzD" \ 7 | --seed "? ? ? ethics vapor struggle ramp dune join nothing wait length" 8 | ``` 9 | 10 | Before starting recovery `seedcat` displays the configuration preview: 11 | 12 | ``` 13 | ============ Seedcat Configuration ============ 14 | P2PKH (Legacy) Address: 1AtD3g5AmR4fMsCRa1haNGmvCTVWq7YfzD 15 | 16 | Derivations: 2 17 | Begin: m/0/0 18 | End: m/44'/0'/0'/0/0 19 | 20 | Seeds: 8.59B 21 | Begin: abandon,abandon,abandon,ethics,vapor,struggle,ramp,dune,join,nothing,wait,length 22 | End: zoo,zoo,zoo,ethics,vapor,struggle,ramp,dune,join,nothing,wait,length 23 | 24 | Total Guesses: 17.2B 25 | ``` 26 | 27 | `Address` can be either `Master XPUB`, `P2PKH`, `P2SH-P2WPKH`, or `P2WPKH` 28 | - The address is determined based on whether it starts with `xpub661MyMwAqRbc`, `1`, `3`, or `bc1` respectively 29 | - We recommend using `XPUB` which offers ~2x the speed and works on non-standard derivation paths and scripts 30 | - Standard derivation paths are chosen that assume you provided your first wallet address (a path ending in `/0`) 31 | - If you are unsure which derivation path your address is from check [your wallet documentation](https://walletsrecovery.org/) 32 | - For custom derivation paths see the [derivations section](#derivations) 33 | 34 | `Seeds` shows how many different combinations of seed words `seedcat` will attempt 35 | - We are using `?` to guess all `2048` possible seed words starting at `abandon` and ending with `zoo` 36 | - Since we are guessing 3 words with 2 derivations the `Total Guesses` is `2048 * 2048 * 2048 * 2` 37 | 38 | `?` wildcards can be used with letters to constrain the words guessed 39 | - For instance, the word `donkey` will be guessed with `do?` or `?key` or `?onk?` 40 | - You can also separate different guesses with `|` such as `do?|da?` 41 | 42 | With today's hardware if you are completely missing more than 4 seed words then recovery is impossible. 43 | If you know some information about the missing seed words (such as the first letter) then recovery should be possible. 44 | 45 | Let's try again with constraints on the second word: 46 | ```bash 47 | seedcat --address "1AtD3g5AmR4fMsCRa1haNGmvCTVWq7YfzD" \ 48 | --seed "? do?|da? ? ethics vapor struggle ramp dune join nothing wait length" 49 | ``` 50 | 51 | Use constraints to make seed recovery faster: 52 | 53 | ``` 54 | Seeds: 96.5M 55 | Begin: abandon,doctor,abandon,ethics,vapor,struggle,ramp,dune,join,nothing,wait,length 56 | End: zoo,day,zoo,ethics,vapor,struggle,ramp,dune,join,nothing,wait,length 57 | 58 | Total Guesses: 193M 59 | 60 | Continue with recovery [Y/n]? 61 | ``` 62 | 63 | The number of guesses is reduced from `17.2B` to `193M` making our recovery **~90x** faster! 64 | 65 | Once you choose to continue you will see updates from the recovery status: 66 | ``` 67 | ============ Seedcat Recovery ============ 68 | Writing Hashes 100.00% (1/1) 69 | 70 | Waiting for GPU initialization please be patient... 71 | * Device #1: NVIDIA GeForce RTX 3090, 22976/24237 MB, 82MCU 72 | 73 | Recovery Guesses 74 | Progress: 27.85% (53.7M/193M) 75 | Speed....: 5.97M/sec 76 | GPU Speed: 187K/sec 77 | ETA......: 23 secs 78 | Elapsed..: 9 secs 79 | 80 | Found Seed: toy,donkey,chaos,ethics,vapor,struggle,ramp,dune,join,nothing,wait,length 81 | ``` 82 | 83 | We were able to guess `donkey` as the second word alongside `toy` and `chaos`...success! 84 | 85 | # Permuting Seeds 86 | If you are unsure about the order of the words you can try different permutations of words. 87 | - Use the `--combinations N` to guess every permutation with a seed phrase length of `N` 88 | - You can pass in more than `N` words and those words will be included in the permutations 89 | - The `^` symbol will anchor a word at its current position within the phrase 90 | 91 | For instance, perhaps you are only sure that the first 3 words of the seed phrase are in correct order: 92 | 93 | ```bash 94 | seedcat --address "1AtD3g5AmR4fMsCRa1haNGmvCTVWq7YfzD" \ 95 | --combinations 12 --seed "^toy ^donkey ^chaos zoo vapor struggle zone nothing join ethics ramp wait length dune" 96 | ``` 97 | Note that we are passing in 14 words instead of 12 and all the words get permuted (except for the first 3 anchored words): 98 | ``` 99 | Seeds: 20.0M 100 | Begin: toy,donkey,chaos,zoo,vapor,struggle,zone,nothing,join,ethics,ramp,wait 101 | End: toy,donkey,chaos,dune,length,wait,ramp,ethics,join,nothing,zone,struggle 102 | ``` 103 | Our result excludes the unused words `zoo` and `zone` while descrambling the rest of the phrase: 104 | ``` 105 | Found Seed: toy,donkey,chaos,ethics,vapor,struggle,ramp,dune,join,nothing,wait,length 106 | ``` 107 | 108 | Note you may use the `?` wildcard with any of the permuted or anchored words. 109 | 110 | Using `^` anchors greatly reduces the number of guesses that `seedcat` needs to make. 111 | 112 | # Passphrase Recovery 113 | Bitcoin passphrases (sometimes misleadingly called the 25th word) are arbitrary strings of text that are added to your seed words. 114 | 115 | Wallets often prompt users to back up their seed words, but users may be tempted to memorize their passphrases leading to possible loss. 116 | 117 | The `--passphrase` option allows you to specify how to attack the passphrase 118 | - **Mask attacks** allow you to use [hashcat wildcards](https://hashcat.net/wiki/doku.php?id=mask_attack) such as `?d` for digits and `?l` for lowercase letters 119 | - **Dictionary attacks** allow you to specify newline-separated text files containing words to try 120 | - You can use the `--passphrase` option twice to combine attacks 121 | - Guessing both seed words and passphrases is possible but multiplies the number of guesses 122 | 123 | ## Mask attacks 124 | If you need to guess a passphrase `"secret"` followed by 3 digits using `--passphrase` argument: 125 | 126 | ```bash 127 | seedcat --address "1Aa7DosYfoYJwZDmMPPTqtH7dXUehYbyMu" \ 128 | --seed "toy donkey chaos ethics vapor struggle ramp dune join nothing wait length" \ 129 | --passphrase "secret?d?d?d" 130 | ``` 131 | 132 | Just as with seed guessing we get a preview of what passphrases will be guessed: 133 | ``` 134 | Passphrases: 1.00K 135 | Begin: secret000 136 | End: secret999 137 | ``` 138 | 139 | If the recovery is successful then the passphrase will be output alongside the seed: 140 | ``` 141 | Found Seed: toy,donkey,chaos,ethics,vapor,struggle,ramp,dune,join,nothing,wait,length 142 | Found Passphrase: secret123 143 | ``` 144 | 145 | ## Dictionary attacks 146 | Dictionary attacks require you have a text file in the `seedcat` folder. We provide english dictionaries of various lengths (sorted by word frequency) in the `seedcat/dicts` folder you can use. 147 | - Specify a dictionary file using the relative path starting with `./` and separated by `/` 148 | - We use this format regardless of your platform so that commands are portable 149 | - To separate multiple dictionaries or add text delimiters use `,` 150 | 151 | If you want to guess 1 lowercase word and 1 uppercase word separated by `"-"` using the `--passphrase` argument: 152 | ```bash 153 | seedcat --address "1CahNjsc2Lw46q1WgvmbQYkLon4NvHhcYw" \ 154 | --seed "toy donkey chaos ethics vapor struggle ramp dune join nothing wait length" \ 155 | --passphrase "./dicts/1k.txt,-,./dicts/1k_upper.txt" 156 | ``` 157 | 158 | Since both files contain `1000` words the number of guesses will be `1000 * 1000`: 159 | ```` 160 | Passphrases: 1.00M 161 | Begin: the-THE 162 | End: entry-ENTRY 163 | ```` 164 | 165 | The result: 166 | ``` 167 | Found Seed: toy,donkey,chaos,ethics,vapor,struggle,ramp,dune,join,nothing,wait,length 168 | Found Passphrase: best-PRACTICE 169 | ``` 170 | 171 | Note that a single dictionary attack is limited to 1 billion guesses. 172 | 173 | ## Combining attacks 174 | You may wish to combine attacks to try a dictionary of words followed by wildcards or to combine 2 dictionary attacks. 175 | 176 | For example if you want to guess 3 letters followed by `" "` and an unknown word: 177 | ```bash 178 | seedcat --address "1CUFN2jAH3FVcBUU1r4qadHnhvo7Ywsi1v" \ 179 | --seed "toy donkey chaos ethics vapor struggle ramp dune join nothing wait length" \ 180 | --passphrase "?u?u?u " --passphrase "./dicts/1k_cap.txt" 181 | ``` 182 | 183 | The preview reveals we are guessing `A-Z` followed by a word from the dictionary: 184 | ``` 185 | Passphrases: 17.6M 186 | Begin: AAA the 187 | End: ZZZ entry 188 | ``` 189 | 190 | The result: 191 | ``` 192 | Found Seed: toy,donkey,chaos,ethics,vapor,struggle,ramp,dune,join,nothing,wait,length 193 | Found Passphrase: ABC Books 194 | ``` 195 | 196 | # Derivations 197 | Derivations are chosen by default based on your address, however some wallets use non-standard derivation paths. 198 | - Every derivation path increases the number of guesses so try to use only 1 if possible 199 | - Or for the fastest speed use `XPUB` which doesn't use derivations at all 200 | - The [mnemonic code converter](https://iancoleman.io/bip39/) provides an useful demo of standard address derivations 201 | 202 | You can pass in a custom derivation path using the `--derivation` option. 203 | - The `?` before a number will try every derivation up to that depth 204 | - To specify a hardened path use `h` or `'` after the number 205 | - You can try multiple derivations separated by `space` or `,` 206 | 207 | For example, suppose you are unsure whether your wallet uses BIP32 or BIP44 and you think your address is one of the first 5 paths: 208 | ```bash 209 | seedcat --address "1NgqeNE2EfBthz4enLb7vs1bapDEQbbivT" \ 210 | --seed "toy donkey chaos ethics vapor struggle ramp dune join nothing wait ?" \ 211 | --derivation "m/0/?4 m/44h/0h/0h/0/?4" 212 | ``` 213 | 214 | This will attempt all 10 derivations `m/0/0`, `m/0/1`, ..., `m/44h/0h/0h/0/3`, `m/44h/0h/0h/0/4` which increases the number of guesses: 215 | ``` 216 | Derivations: 10 217 | Begin: m/0/0 218 | End: m/44h/0h/0h/0/4 219 | 220 | Total Guesses: 20.5K 221 | ``` 222 | 223 | Since we are guessing one word with 10 derivations the `Total Guesses` is `10 * 2048` 224 | -------------------------------------------------------------------------------- /src/permutations.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::min; 2 | use std::collections::BTreeMap; 3 | use std::fmt::Debug; 4 | 5 | /*** 6 | Generates PERMUTE(N, K) permutations where you want to select all K! permutations from a 7 | possible set of length N. We generate lexicographic permutations so that the work 8 | can be multi-threaded without locking. 9 | 10 | If you are performing any expensive operations on permutations the multi-threaded code will 11 | be orders of magnitude faster on multiple cores. 12 | 13 | See algorithms from here: 14 | https://www.codeproject.com/Articles/1250925/Permutations-Fast-implementations-and-a-new-indexi 15 | */ 16 | #[derive(Debug, Clone)] 17 | pub struct Permutations { 18 | elements: Vec, 19 | indices: Vec, 20 | combination_index: u64, 21 | permutation_index: u64, 22 | len: u64, 23 | k_permutations: u64, 24 | k: usize, 25 | index: u64, 26 | } 27 | 28 | impl Permutations { 29 | pub fn new(elements: Vec, k: usize) -> Self { 30 | let n = elements.len(); 31 | Self::new_shard(elements, k, 0, n_permute_k(n, k)) 32 | } 33 | 34 | fn new_shard(elements: Vec, k: usize, index: u64, len: u64) -> Self { 35 | let k_permutations = n_permute_k(k, k); 36 | let combination_index = index / k_permutations; 37 | let permutation_index = index % k_permutations; 38 | 39 | Self { 40 | elements, 41 | indices: vec![], 42 | combination_index, 43 | permutation_index, 44 | len, 45 | k_permutations, 46 | k, 47 | index, 48 | } 49 | } 50 | 51 | pub fn shard(&self, num: usize) -> Vec> { 52 | let shard_size = self.len / num as u64; 53 | let mut shards = vec![]; 54 | let mut index = 0; 55 | 56 | while index < self.len { 57 | let len = min(self.len, index + shard_size); 58 | shards.push(Self::new_shard(self.elements.clone(), self.k, index, len)); 59 | index += shard_size; 60 | } 61 | shards 62 | } 63 | 64 | pub fn len(&self) -> u64 { 65 | self.len 66 | } 67 | 68 | pub fn next(&mut self) -> Option<&Vec> { 69 | if self.indices.is_empty() { 70 | self.next_combo(); 71 | return Some(&self.indices); 72 | } 73 | 74 | self.index += 1; 75 | 76 | if self.index >= self.len { 77 | return None; 78 | } 79 | 80 | if self.index < self.len { 81 | self.next_perm(); 82 | } 83 | return Some(&self.indices); 84 | } 85 | 86 | fn next_combo(&mut self) { 87 | let n = self.elements.len(); 88 | let indices = indexed_combination(self.combination_index, n, self.k); 89 | let mut combo = vec![]; 90 | for index in indices { 91 | combo.push(self.elements[index].clone()); 92 | } 93 | self.indices = indexed_permutation(self.permutation_index, combo); 94 | } 95 | 96 | fn next_perm(&mut self) { 97 | if self.permutation_index == self.k_permutations - 1 { 98 | self.combination_index += 1; 99 | self.permutation_index = 0; 100 | self.next_combo(); 101 | } else { 102 | self.permutation_index += 1; 103 | next_permutation(&mut self.indices); 104 | } 105 | } 106 | } 107 | 108 | /// Precomputed factorial counts 109 | const FACTORIAL: [u64; 21] = [ 110 | 1, 111 | 1, 112 | 2, 113 | 6, 114 | 24, 115 | 120, 116 | 720, 117 | 5040, 118 | 40320, 119 | 362880, 120 | 3628800, 121 | 39916800, 122 | 479001600, 123 | 6227020800, 124 | 87178291200, 125 | 1307674368000, 126 | 20922789888000, 127 | 355687428096000, 128 | 6402373705728000, 129 | 121645100408832000, 130 | 2432902008176640000, 131 | ]; 132 | 133 | /// Fast generation of the next lexicographical permutation 134 | fn next_permutation(list: &mut Vec) -> bool { 135 | let mut largest_index = usize::MAX; 136 | 137 | for i in (0..list.len() - 1).rev() { 138 | if list[i] < list[i + 1] { 139 | largest_index = i; 140 | break; 141 | } 142 | } 143 | 144 | if largest_index == usize::MAX { 145 | return false; 146 | } 147 | 148 | let mut largest_index2 = usize::MAX; 149 | for i in (0..list.len()).rev() { 150 | if list[largest_index] < list[i] { 151 | largest_index2 = i; 152 | break; 153 | } 154 | } 155 | 156 | list.swap(largest_index, largest_index2); 157 | 158 | let mut i = largest_index + 1; 159 | let mut j = list.len() - 1; 160 | while i < j { 161 | list.swap(i, j); 162 | i += 1; 163 | j -= 1; 164 | } 165 | 166 | return true; 167 | } 168 | 169 | fn n_permute_k(n: usize, k: usize) -> u64 { 170 | let mut end = 1_u64; 171 | for i in n - k + 1..=n { 172 | end = end.saturating_mul(i as u64); 173 | } 174 | end 175 | } 176 | /// Returns N choose K 177 | fn n_choose_k(n: usize, k: usize) -> u64 { 178 | if k > n { 179 | 0 180 | } else if n <= 20 { 181 | // Optimization when fitting into u64 182 | FACTORIAL[n] / FACTORIAL[k] / FACTORIAL[n - k] 183 | } else { 184 | let end = k.min(n - k) as u64; 185 | (1..=end).fold(1, |acc, val| acc * (n as u64 - val + 1) / val) 186 | } 187 | } 188 | 189 | /// Returns an indexed combination from k choose 0..n 190 | /// 0 <= i < ncr(n, k) 191 | fn indexed_combination(i: u64, n: usize, k: usize) -> Vec { 192 | assert!(n >= k); 193 | assert!(i < n_choose_k(n, k)); 194 | let mut combo = vec![]; 195 | let mut r = i + 1; 196 | let mut j = 0; 197 | for s in 1..k + 1 { 198 | let mut cs = j + 1; 199 | 200 | while r > n_choose_k(n - cs, k - s) { 201 | r -= n_choose_k(n - cs, k - s); 202 | cs += 1; 203 | } 204 | combo.push(cs - 1); 205 | j = cs; 206 | } 207 | combo 208 | } 209 | 210 | /// Slow generation of a lexicographical permutation at a given index 211 | fn indexed_permutation(index: u64, mut list: Vec) -> Vec { 212 | let size = list.len(); 213 | assert!(index < FACTORIAL[size]); 214 | list.sort(); 215 | 216 | let mut used = vec![false; size]; 217 | let mut lower = FACTORIAL[size]; 218 | let mut result_indices = BTreeMap::new(); 219 | 220 | for i in 0..size { 221 | let bigger = lower; 222 | lower = FACTORIAL[size - i - 1]; 223 | let mut counter = (index % bigger / lower) as isize; 224 | let mut result_index = 0; 225 | 'outer: loop { 226 | if !used[result_index] { 227 | counter -= 1; 228 | if counter < 0 { 229 | break 'outer; 230 | } 231 | } 232 | result_index += 1; 233 | } 234 | result_indices.insert(result_index, i); 235 | used[result_index] = true; 236 | } 237 | 238 | let mut result = BTreeMap::new(); 239 | for (index, element) in list.drain(..).enumerate() { 240 | result.insert(result_indices[&index], element); 241 | } 242 | result.into_iter().map(|(_, element)| element).collect() 243 | } 244 | 245 | #[cfg(test)] 246 | mod tests { 247 | use std::collections::HashSet; 248 | 249 | use crate::permutations::*; 250 | 251 | #[test] 252 | fn test_valid_shards() { 253 | let permutations = Permutations::new(vec![1, 2, 3], 2); 254 | let shards = assert_explode(permutations.shard(2)); 255 | assert_eq!(assert_explode(vec![permutations]), shards); 256 | 257 | let permutations = Permutations::new(vec![1, 2, 3], 2); 258 | let shards = assert_explode(permutations.shard(6)); 259 | assert_eq!(assert_explode(vec![permutations]), shards); 260 | 261 | let permutations = Permutations::new(vec![1, 2, 3, 4, 5, 6, 7, 8, 9], 5); 262 | let shards = assert_explode(permutations.shard(10)); 263 | assert_eq!(assert_explode(vec![permutations]), shards); 264 | } 265 | 266 | fn assert_explode(permutations: Vec>) -> Vec> { 267 | let mut all = vec![]; 268 | let mut set = HashSet::new(); 269 | for mut permutation in permutations { 270 | while let Some(next) = permutation.next() { 271 | assert_eq!(set.contains(next), false); 272 | set.insert(next.clone()); 273 | all.push(next.clone()); 274 | } 275 | } 276 | all 277 | } 278 | 279 | #[test] 280 | fn test_permutations_of_k() { 281 | let mut perm = Permutations::new(vec![1, 2, 3], 2); 282 | assert_eq!(perm.next(), Some(&vec![1, 2])); 283 | assert_eq!(perm.next(), Some(&vec![2, 1])); 284 | assert_eq!(perm.next(), Some(&vec![1, 3])); 285 | assert_eq!(perm.next(), Some(&vec![3, 1])); 286 | assert_eq!(perm.next(), Some(&vec![2, 3])); 287 | assert_eq!(perm.next(), Some(&vec![3, 2])); 288 | assert_eq!(perm.next(), None); 289 | } 290 | 291 | #[test] 292 | fn test_indexed_combo() { 293 | assert_eq!(indexed_combination(0, 4, 2), vec![0, 1]); 294 | assert_eq!(indexed_combination(1, 4, 2), vec![0, 2]); 295 | assert_eq!(indexed_combination(2, 4, 2), vec![0, 3]); 296 | assert_eq!(indexed_combination(3, 4, 2), vec![1, 2]); 297 | assert_eq!(indexed_combination(4, 4, 2), vec![1, 3]); 298 | assert_eq!(indexed_combination(5, 4, 2), vec![2, 3]); 299 | assert_eq!( 300 | indexed_combination(173103094564, 100, 10), 301 | vec![0, 2, 4, 10, 18, 24, 37, 65, 79, 82] 302 | ); 303 | } 304 | 305 | #[test] 306 | fn test_counts() { 307 | assert_eq!(n_choose_k(10, 5), 252); 308 | assert_eq!(n_choose_k(24, 10), 1961256); 309 | assert_eq!(n_choose_k(30, 20), 30045015); 310 | assert_eq!(n_permute_k(10, 5), 30240); 311 | assert_eq!(n_permute_k(24, 10), 7117005772800); 312 | } 313 | 314 | #[test] 315 | fn test_indexed_permutation() { 316 | assert_eq!(indexed_permutation(0, vec![1, 2, 3]), vec![1, 2, 3]); 317 | assert_eq!(indexed_permutation(1, vec![1, 2, 3]), vec![1, 3, 2]); 318 | assert_eq!(indexed_permutation(2, vec![1, 2, 3]), vec![2, 1, 3]); 319 | assert_eq!(indexed_permutation(3, vec![1, 2, 3]), vec![2, 3, 1]); 320 | assert_eq!(indexed_permutation(4, vec![1, 2, 3]), vec![3, 1, 2]); 321 | assert_eq!(indexed_permutation(5, vec![1, 2, 3]), vec![3, 2, 1]); 322 | } 323 | 324 | #[test] 325 | fn test_next_permutation() { 326 | let mut list = vec![1, 2, 3]; 327 | assert_eq!(next_permutation(&mut list), true); 328 | assert_eq!(list, vec![1, 3, 2]); 329 | assert_eq!(next_permutation(&mut list), true); 330 | assert_eq!(list, vec![2, 1, 3]); 331 | assert_eq!(next_permutation(&mut list), true); 332 | assert_eq!(list, vec![2, 3, 1]); 333 | assert_eq!(next_permutation(&mut list), true); 334 | assert_eq!(list, vec![3, 1, 2]); 335 | assert_eq!(next_permutation(&mut list), true); 336 | assert_eq!(list, vec![3, 2, 1]); 337 | assert_eq!(next_permutation(&mut list), false); 338 | } 339 | } 340 | -------------------------------------------------------------------------------- /src/address.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter}; 2 | use std::str::FromStr; 3 | 4 | use crate::logger::Attempt; 5 | use anyhow::{bail, format_err, Result}; 6 | use bitcoin::bip32::{ChildNumber, Xpub}; 7 | use bitcoin::{Address, Network}; 8 | 9 | const MAX_DERIVATIONS: usize = 100; 10 | 11 | #[derive(Debug, Clone, Eq, PartialEq)] 12 | pub struct AddressValid { 13 | pub formatted: String, 14 | pub kind: AddressKind, 15 | pub derivations: Derivations, 16 | } 17 | 18 | #[derive(Debug, Clone, Eq, PartialEq)] 19 | pub struct Derivations { 20 | derivations: Vec, 21 | args: Vec, 22 | } 23 | 24 | impl Derivations { 25 | /// Args that are exploded in the hashes file 26 | pub fn args(&self) -> Vec { 27 | self.args.clone() 28 | } 29 | 30 | /// Number of args that are exploded inside hashcat 31 | pub fn hash_ratio(&self) -> f64 { 32 | self.derivations.len() as f64 / self.args.len() as f64 33 | } 34 | } 35 | 36 | impl Attempt for Derivations { 37 | fn total(&self) -> u64 { 38 | self.derivations.len() as u64 39 | } 40 | 41 | fn begin(&self) -> String { 42 | self.derivations.first().expect("exists").clone() 43 | } 44 | 45 | fn end(&self) -> String { 46 | self.derivations.last().expect("exists").clone() 47 | } 48 | } 49 | 50 | const ERR_MSG: &str = "\nDerivation path should be valid comma or path-separated: 51 | Address #0 from an unhardened path: 'm/0/0' 52 | Address #2 from a hardened path: 'm/44h/0h/0h/0/2' 53 | You can try multiple paths: 'm/0/0,m/44h/0h/0h/0/0' 54 | '?' attempts all paths from 0-11: 'm/0/?11' 55 | 56 | Master XPUB does not require a derivation path and is ~2x faster to guess 57 | Try to use the exact derivation path for the address you have (see https://walletsrecovery.org/)\n"; 58 | 59 | impl AddressValid { 60 | pub fn from_arg(address: &str, derivation: &Option) -> Result { 61 | let kind = Self::kind(&address)?; 62 | 63 | if kind.is_xpub && derivation.is_some() { 64 | bail!("XPUBs do not require a derivation path to be specified"); 65 | } 66 | 67 | let derivations = Self::derivation(&kind, derivation, MAX_DERIVATIONS)?; 68 | 69 | Ok(Self::new(address.to_string(), kind, derivations)) 70 | } 71 | 72 | pub fn new(formatted: String, kind: AddressKind, derivations: Derivations) -> Self { 73 | Self { 74 | formatted, 75 | kind, 76 | derivations, 77 | } 78 | } 79 | 80 | fn kind(address: &str) -> Result { 81 | let strs: Vec<_> = address_kinds().iter().map(|k| format!("\t{}", k)).collect(); 82 | let error = format!("You must use one of the following formats (https://en.bitcoin.it/wiki/List_of_address_prefixes)\n{}", strs.join("\n")); 83 | 84 | for kind in address_kinds() { 85 | if address.starts_with(&kind.start) { 86 | if kind.is_xpub { 87 | match Xpub::from_str(&address) { 88 | Ok(xpub) if is_master(xpub) => return Ok(kind.clone()), 89 | Ok(_) => bail!( 90 | "Xpub is not a master public key (use an address instead)\n{}", 91 | error 92 | ), 93 | Err(_) => bail!("Xpub is not correctly encoded\n{}", error), 94 | } 95 | } else { 96 | match Address::from_str(&address) { 97 | Ok(_) => return Ok(kind.clone()), 98 | Err(_) => bail!("Address is not correctly encoded\n{}", error), 99 | } 100 | } 101 | } 102 | } 103 | 104 | bail!(error); 105 | } 106 | 107 | fn derivation( 108 | kind: &AddressKind, 109 | arg: &Option, 110 | max_derivations: usize, 111 | ) -> Result { 112 | let split = match arg { 113 | None => kind.derivations.clone(), 114 | Some(arg) => { 115 | let args = if arg.contains(",") { 116 | arg.split(",") 117 | } else if arg.contains("|") { 118 | arg.split("|") 119 | } else { 120 | arg.split(" ") 121 | }; 122 | args.map(|s| s.to_string()).collect() 123 | } 124 | }; 125 | 126 | let mut derivations = vec![]; 127 | let mut args = vec![]; 128 | for derivation in split.clone() { 129 | let derivation = match derivation.strip_prefix("m/") { 130 | None => bail!( 131 | "Derivation path '{}' must start with 'm/'{}", 132 | derivation, 133 | ERR_MSG 134 | ), 135 | Some(str) => str, 136 | }; 137 | 138 | let (derivation, arg) = 139 | Self::derivation_paths(derivation, derivations.len(), max_derivations)?; 140 | derivations.extend(derivation); 141 | 142 | if derivations.len() <= max_derivations && args.len() > 0 { 143 | args = Self::extend_paths(&args, &arg, ","); 144 | } else { 145 | args.extend(arg); 146 | } 147 | } 148 | 149 | Ok(Derivations { derivations, args }) 150 | } 151 | 152 | fn derivation_paths( 153 | derivation: &str, 154 | num_args: usize, 155 | max_derivations: usize, 156 | ) -> Result<(Vec, Vec)> { 157 | let mut derivations = vec!["m".to_string()]; 158 | let mut args = vec!["m".to_string()]; 159 | 160 | for path in derivation.split("/").into_iter() { 161 | let nodes = Self::derivation_nodes(path).map_err(|err| { 162 | format_err!( 163 | "Bad element in derivation path '{}' {}{}", 164 | derivation, 165 | err, 166 | ERR_MSG 167 | ) 168 | })?; 169 | 170 | derivations = Self::extend_paths(&derivations, &nodes, "/"); 171 | 172 | if num_args + derivations.len() > max_derivations { 173 | args = Self::extend_paths(&args, &nodes, "/"); 174 | } else { 175 | args = Self::extend_paths(&args, &vec![path.to_string()], "/"); 176 | } 177 | } 178 | 179 | return Ok((derivations, args)); 180 | } 181 | 182 | fn extend_paths(current: &Vec, nodes: &Vec, delim: &str) -> Vec { 183 | let mut tmp = vec![]; 184 | for node in nodes.clone().into_iter() { 185 | for out in current.clone().into_iter() { 186 | tmp.push(format!("{}{}{}", out, delim, node)); 187 | } 188 | } 189 | tmp 190 | } 191 | 192 | fn derivation_nodes(path: &str) -> Result> { 193 | let mut suffix = "".to_string(); 194 | let mut question = "".to_string(); 195 | let mut node = path.chars(); 196 | 197 | if path.ends_with("h") || path.ends_with("'") { 198 | suffix = node.next_back().unwrap().to_string(); 199 | } 200 | if path.starts_with("?") { 201 | question = node.next().unwrap().to_string(); 202 | } 203 | 204 | return match node.as_str().parse::() { 205 | Ok(num) if question.is_empty() => Ok(vec![format!("{}{}", num, suffix)]), 206 | Ok(num) => Ok((0..=num).map(|i| format!("{}{}", i, suffix)).collect()), 207 | Err(_) => bail!("invalid number '{}'", node.as_str()), 208 | }; 209 | } 210 | } 211 | 212 | pub fn address_kinds() -> Vec { 213 | vec![ 214 | AddressKind::new( 215 | "XPUB", 216 | "Master Extended Pubic Key", 217 | "xpub", 218 | vec!["m/0".to_string()], 219 | true, 220 | ), 221 | AddressKind::new( 222 | "P2PKH", 223 | "Legacy", 224 | "1", 225 | vec!["m/0/0".to_string(), "m/44'/0'/0'/0/0".to_string()], 226 | false, 227 | ), 228 | AddressKind::new( 229 | "P2SH-P2WPKH", 230 | "Nested Segwit", 231 | "3", 232 | vec!["m/0/0".to_string(), "m/49'/0'/0'/0/0".to_string()], 233 | false, 234 | ), 235 | AddressKind::new( 236 | "P2WPKH", 237 | "Native Segwit", 238 | "bc1", 239 | vec!["m/84'/0'/0'/0/0".to_string()], 240 | false, 241 | ), 242 | ] 243 | } 244 | 245 | fn is_master(xpub: Xpub) -> bool { 246 | return xpub.network == Network::Bitcoin 247 | && xpub.depth == 0 248 | && xpub.child_number == ChildNumber::from(0); 249 | } 250 | 251 | #[derive(Debug, Clone, Eq, PartialEq)] 252 | pub struct AddressKind { 253 | pub key: String, 254 | pub name: String, 255 | start: String, 256 | derivations: Vec, 257 | is_xpub: bool, 258 | } 259 | 260 | impl AddressKind { 261 | fn new(key: &str, name: &str, start: &str, derivations: Vec, is_xpub: bool) -> Self { 262 | Self { 263 | key: key.to_string(), 264 | name: name.to_string(), 265 | start: start.to_string(), 266 | derivations, 267 | is_xpub, 268 | } 269 | } 270 | } 271 | 272 | impl Display for AddressKind { 273 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 274 | write!(f, "{:<10}", format!("{}...", self.start))?; 275 | write!(f, "{:<15}", self.key)?; 276 | write!(f, "{}", self.name)?; 277 | Ok(()) 278 | } 279 | } 280 | 281 | #[cfg(test)] 282 | mod tests { 283 | use crate::address::*; 284 | 285 | #[test] 286 | fn parses_addresses() { 287 | let kind = AddressValid::kind("1111111111111111111114oLvT2").unwrap(); 288 | assert_eq!(kind.key, "P2PKH"); 289 | 290 | let kind = AddressValid::kind("3AzWUwL8YYci6ZAjAfd6mzzKDAmsCWB7Nr").unwrap(); 291 | assert_eq!(kind.key, "P2SH-P2WPKH"); 292 | 293 | let kind = AddressValid::kind("bc1q3zn9axe5k3tptupymypjzheuxf8r9yp7zutulg").unwrap(); 294 | assert_eq!(kind.key, "P2WPKH"); 295 | 296 | let kind = AddressValid::kind("xpub661MyMwAqRbcG95rS28rhZiknMvbUJhPpEWgMUbWa4xjMEc12aVewXf7fey3rGD9Sef81NXqTd1vyYToRokkiU9BTz6u5UXmikfNHTV9oCT").unwrap(); 297 | assert_eq!(kind.key, "XPUB"); 298 | 299 | // non-master xpub 300 | let kind = AddressValid::kind("xpub6878MZDSpciXuNC2cRRBa6dZsgBeE8UYaFDqA1uTazMaYdR1Xq7HFHBC3FpcFHiMytkmrMVBQKi3Wx2wT9xAn8mxuMeqtJG8TPDcpyfTk2J"); 301 | assert!(kind.is_err()); 302 | } 303 | 304 | #[test] 305 | fn parses_derivations() { 306 | let kind = AddressKind::new("", "", "", vec!["m/123".to_string()], false); 307 | let derivation = AddressValid::derivation(&kind, &None, 1).unwrap(); 308 | assert_eq!(derivation.args(), vec!["m/123".to_string()]); 309 | 310 | let derivation = AddressValid::derivation(&kind, &Some("m/0,m/1'".to_string()), 1).unwrap(); 311 | assert_eq!(derivation.args(), vec!["m/0", "m/1'"]); 312 | 313 | let derivation = 314 | AddressValid::derivation(&kind, &Some("m/0 m/1/?2".to_string()), 10).unwrap(); 315 | assert_eq!(derivation.args(), vec!["m/0,m/1/?2".to_string()]); 316 | assert_eq!(derivation.begin(), "m/0"); 317 | assert_eq!(derivation.end(), "m/1/2"); 318 | assert_eq!(derivation.total(), 4); 319 | assert_eq!(derivation.hash_ratio(), 4.0); 320 | 321 | assert!(AddressValid::derivation(&kind, &Some("z/?2".to_string()), 1).is_err()); 322 | 323 | // splits if over 10 324 | let derivation = 325 | AddressValid::derivation(&kind, &Some("m/?9'/9/?9|m/0/0".to_string()), 10).unwrap(); 326 | assert_eq!(derivation.begin(), "m/0'/9/0"); 327 | assert_eq!(derivation.end(), "m/0/0"); 328 | assert_eq!(derivation.total(), 101); 329 | assert_eq!(derivation.hash_ratio(), 101.0 / 11.0); 330 | assert_eq!( 331 | derivation.args, 332 | vec![ 333 | "m/?9'/9/0", 334 | "m/?9'/9/1", 335 | "m/?9'/9/2", 336 | "m/?9'/9/3", 337 | "m/?9'/9/4", 338 | "m/?9'/9/5", 339 | "m/?9'/9/6", 340 | "m/?9'/9/7", 341 | "m/?9'/9/8", 342 | "m/?9'/9/9", 343 | "m/0/0", 344 | ] 345 | ); 346 | } 347 | } 348 | -------------------------------------------------------------------------------- /dicts/1k.txt: -------------------------------------------------------------------------------- 1 | the 2 | of 3 | and 4 | to 5 | a 6 | in 7 | for 8 | is 9 | on 10 | that 11 | by 12 | this 13 | with 14 | i 15 | you 16 | it 17 | not 18 | or 19 | be 20 | are 21 | from 22 | at 23 | as 24 | your 25 | all 26 | have 27 | new 28 | more 29 | an 30 | was 31 | we 32 | will 33 | home 34 | can 35 | us 36 | about 37 | if 38 | page 39 | my 40 | has 41 | search 42 | free 43 | but 44 | our 45 | one 46 | other 47 | do 48 | no 49 | information 50 | time 51 | they 52 | site 53 | he 54 | up 55 | may 56 | what 57 | which 58 | their 59 | news 60 | out 61 | use 62 | any 63 | there 64 | see 65 | only 66 | so 67 | his 68 | when 69 | contact 70 | here 71 | business 72 | who 73 | web 74 | also 75 | now 76 | help 77 | get 78 | pm 79 | view 80 | online 81 | c 82 | e 83 | first 84 | am 85 | been 86 | would 87 | how 88 | were 89 | me 90 | s 91 | services 92 | some 93 | these 94 | click 95 | its 96 | like 97 | service 98 | x 99 | than 100 | find 101 | price 102 | date 103 | back 104 | top 105 | people 106 | had 107 | list 108 | name 109 | just 110 | over 111 | state 112 | year 113 | day 114 | into 115 | email 116 | two 117 | health 118 | n 119 | world 120 | re 121 | next 122 | used 123 | go 124 | b 125 | work 126 | last 127 | most 128 | products 129 | music 130 | buy 131 | data 132 | make 133 | them 134 | should 135 | product 136 | system 137 | post 138 | her 139 | city 140 | t 141 | add 142 | policy 143 | number 144 | such 145 | please 146 | available 147 | copyright 148 | support 149 | message 150 | after 151 | best 152 | software 153 | then 154 | jan 155 | good 156 | video 157 | well 158 | d 159 | where 160 | info 161 | rights 162 | public 163 | books 164 | high 165 | school 166 | through 167 | m 168 | each 169 | links 170 | she 171 | review 172 | years 173 | order 174 | very 175 | privacy 176 | book 177 | items 178 | company 179 | r 180 | read 181 | group 182 | sex 183 | need 184 | many 185 | user 186 | said 187 | de 188 | does 189 | set 190 | under 191 | general 192 | research 193 | university 194 | january 195 | mail 196 | full 197 | map 198 | reviews 199 | program 200 | life 201 | know 202 | games 203 | way 204 | days 205 | management 206 | p 207 | part 208 | could 209 | great 210 | united 211 | hotel 212 | real 213 | f 214 | item 215 | international 216 | center 217 | ebay 218 | must 219 | store 220 | travel 221 | comments 222 | made 223 | development 224 | report 225 | off 226 | member 227 | details 228 | line 229 | terms 230 | before 231 | hotels 232 | did 233 | send 234 | right 235 | type 236 | because 237 | local 238 | those 239 | using 240 | results 241 | office 242 | education 243 | national 244 | car 245 | design 246 | take 247 | posted 248 | internet 249 | address 250 | community 251 | within 252 | states 253 | area 254 | want 255 | phone 256 | dvd 257 | shipping 258 | reserved 259 | subject 260 | between 261 | forum 262 | family 263 | l 264 | long 265 | based 266 | w 267 | code 268 | show 269 | o 270 | even 271 | black 272 | check 273 | special 274 | prices 275 | website 276 | index 277 | being 278 | women 279 | much 280 | sign 281 | file 282 | link 283 | open 284 | today 285 | technology 286 | south 287 | case 288 | project 289 | same 290 | pages 291 | uk 292 | version 293 | section 294 | own 295 | found 296 | sports 297 | house 298 | related 299 | security 300 | both 301 | g 302 | county 303 | american 304 | photo 305 | game 306 | members 307 | power 308 | while 309 | care 310 | network 311 | down 312 | computer 313 | systems 314 | three 315 | total 316 | place 317 | end 318 | following 319 | download 320 | h 321 | him 322 | without 323 | per 324 | access 325 | think 326 | north 327 | resources 328 | current 329 | posts 330 | big 331 | media 332 | law 333 | control 334 | water 335 | history 336 | pictures 337 | size 338 | art 339 | personal 340 | since 341 | including 342 | guide 343 | shop 344 | directory 345 | board 346 | location 347 | change 348 | white 349 | text 350 | small 351 | rating 352 | rate 353 | government 354 | children 355 | during 356 | usa 357 | return 358 | students 359 | v 360 | shopping 361 | account 362 | times 363 | sites 364 | level 365 | digital 366 | profile 367 | previous 368 | form 369 | events 370 | love 371 | old 372 | john 373 | main 374 | call 375 | hours 376 | image 377 | department 378 | title 379 | description 380 | non 381 | k 382 | y 383 | insurance 384 | another 385 | why 386 | shall 387 | property 388 | class 389 | cd 390 | still 391 | money 392 | quality 393 | every 394 | listing 395 | content 396 | country 397 | private 398 | little 399 | visit 400 | save 401 | tools 402 | low 403 | reply 404 | customer 405 | december 406 | compare 407 | movies 408 | include 409 | college 410 | value 411 | article 412 | york 413 | man 414 | card 415 | jobs 416 | provide 417 | j 418 | food 419 | source 420 | author 421 | different 422 | press 423 | u 424 | learn 425 | sale 426 | around 427 | print 428 | course 429 | job 430 | canada 431 | process 432 | teen 433 | room 434 | stock 435 | training 436 | too 437 | credit 438 | point 439 | join 440 | science 441 | men 442 | categories 443 | advanced 444 | west 445 | sales 446 | look 447 | english 448 | left 449 | team 450 | estate 451 | box 452 | conditions 453 | select 454 | windows 455 | photos 456 | gay 457 | thread 458 | week 459 | category 460 | note 461 | live 462 | large 463 | gallery 464 | table 465 | register 466 | however 467 | june 468 | october 469 | november 470 | market 471 | library 472 | really 473 | action 474 | start 475 | series 476 | model 477 | features 478 | air 479 | industry 480 | plan 481 | human 482 | provided 483 | tv 484 | yes 485 | required 486 | second 487 | hot 488 | accessories 489 | cost 490 | movie 491 | forums 492 | march 493 | la 494 | september 495 | better 496 | say 497 | questions 498 | july 499 | yahoo 500 | going 501 | medical 502 | test 503 | friend 504 | come 505 | dec 506 | server 507 | pc 508 | study 509 | application 510 | cart 511 | staff 512 | articles 513 | san 514 | feedback 515 | again 516 | play 517 | looking 518 | issues 519 | april 520 | never 521 | users 522 | complete 523 | street 524 | topic 525 | comment 526 | financial 527 | things 528 | working 529 | against 530 | standard 531 | tax 532 | person 533 | below 534 | mobile 535 | less 536 | got 537 | blog 538 | party 539 | payment 540 | equipment 541 | login 542 | student 543 | let 544 | programs 545 | offers 546 | legal 547 | above 548 | recent 549 | park 550 | stores 551 | side 552 | act 553 | problem 554 | red 555 | give 556 | memory 557 | performance 558 | social 559 | q 560 | august 561 | quote 562 | language 563 | story 564 | sell 565 | options 566 | experience 567 | rates 568 | create 569 | key 570 | body 571 | young 572 | america 573 | important 574 | field 575 | few 576 | east 577 | paper 578 | single 579 | ii 580 | age 581 | activities 582 | club 583 | example 584 | girls 585 | additional 586 | password 587 | z 588 | latest 589 | something 590 | road 591 | gift 592 | question 593 | changes 594 | night 595 | ca 596 | hard 597 | texas 598 | oct 599 | pay 600 | four 601 | poker 602 | status 603 | browse 604 | issue 605 | range 606 | building 607 | seller 608 | court 609 | february 610 | always 611 | result 612 | audio 613 | light 614 | write 615 | war 616 | nov 617 | offer 618 | blue 619 | groups 620 | al 621 | easy 622 | given 623 | files 624 | event 625 | release 626 | analysis 627 | request 628 | fax 629 | china 630 | making 631 | picture 632 | needs 633 | possible 634 | might 635 | professional 636 | yet 637 | month 638 | major 639 | star 640 | areas 641 | future 642 | space 643 | committee 644 | hand 645 | sun 646 | cards 647 | problems 648 | london 649 | washington 650 | meeting 651 | rss 652 | become 653 | interest 654 | id 655 | child 656 | keep 657 | enter 658 | california 659 | porn 660 | share 661 | similar 662 | garden 663 | schools 664 | million 665 | added 666 | reference 667 | companies 668 | listed 669 | baby 670 | learning 671 | energy 672 | run 673 | delivery 674 | net 675 | popular 676 | term 677 | film 678 | stories 679 | put 680 | computers 681 | journal 682 | reports 683 | co 684 | try 685 | welcome 686 | central 687 | images 688 | president 689 | notice 690 | god 691 | original 692 | head 693 | radio 694 | until 695 | cell 696 | color 697 | self 698 | council 699 | away 700 | includes 701 | track 702 | australia 703 | discussion 704 | archive 705 | once 706 | others 707 | entertainment 708 | agreement 709 | format 710 | least 711 | society 712 | months 713 | log 714 | safety 715 | friends 716 | sure 717 | faq 718 | trade 719 | edition 720 | cars 721 | messages 722 | marketing 723 | tell 724 | further 725 | updated 726 | association 727 | able 728 | having 729 | provides 730 | david 731 | fun 732 | already 733 | green 734 | studies 735 | close 736 | common 737 | drive 738 | specific 739 | several 740 | gold 741 | feb 742 | living 743 | sep 744 | collection 745 | called 746 | short 747 | arts 748 | lot 749 | ask 750 | display 751 | limited 752 | powered 753 | solutions 754 | means 755 | director 756 | daily 757 | beach 758 | past 759 | natural 760 | whether 761 | due 762 | et 763 | electronics 764 | five 765 | upon 766 | period 767 | planning 768 | database 769 | says 770 | official 771 | weather 772 | mar 773 | land 774 | average 775 | done 776 | technical 777 | window 778 | france 779 | pro 780 | region 781 | island 782 | record 783 | direct 784 | microsoft 785 | conference 786 | environment 787 | records 788 | st 789 | district 790 | calendar 791 | costs 792 | style 793 | url 794 | front 795 | statement 796 | update 797 | parts 798 | aug 799 | ever 800 | downloads 801 | early 802 | miles 803 | sound 804 | resource 805 | present 806 | applications 807 | either 808 | ago 809 | document 810 | word 811 | works 812 | material 813 | bill 814 | apr 815 | written 816 | talk 817 | federal 818 | hosting 819 | rules 820 | final 821 | adult 822 | tickets 823 | thing 824 | centre 825 | requirements 826 | via 827 | cheap 828 | nude 829 | kids 830 | finance 831 | true 832 | minutes 833 | else 834 | mark 835 | third 836 | rock 837 | gifts 838 | europe 839 | reading 840 | topics 841 | bad 842 | individual 843 | tips 844 | plus 845 | auto 846 | cover 847 | usually 848 | edit 849 | together 850 | videos 851 | percent 852 | fast 853 | function 854 | fact 855 | unit 856 | getting 857 | global 858 | tech 859 | meet 860 | far 861 | economic 862 | en 863 | player 864 | projects 865 | lyrics 866 | often 867 | subscribe 868 | submit 869 | germany 870 | amount 871 | watch 872 | included 873 | feel 874 | though 875 | bank 876 | risk 877 | thanks 878 | everything 879 | deals 880 | various 881 | words 882 | linux 883 | jul 884 | production 885 | commercial 886 | james 887 | weight 888 | town 889 | heart 890 | advertising 891 | received 892 | choose 893 | treatment 894 | newsletter 895 | archives 896 | points 897 | knowledge 898 | magazine 899 | error 900 | camera 901 | jun 902 | girl 903 | currently 904 | construction 905 | toys 906 | registered 907 | clear 908 | golf 909 | receive 910 | domain 911 | methods 912 | chapter 913 | makes 914 | protection 915 | policies 916 | loan 917 | wide 918 | beauty 919 | manager 920 | india 921 | position 922 | taken 923 | sort 924 | listings 925 | models 926 | michael 927 | known 928 | half 929 | cases 930 | step 931 | engineering 932 | florida 933 | simple 934 | quick 935 | none 936 | wireless 937 | license 938 | paul 939 | friday 940 | lake 941 | whole 942 | annual 943 | published 944 | later 945 | basic 946 | sony 947 | shows 948 | corporate 949 | google 950 | church 951 | method 952 | purchase 953 | customers 954 | active 955 | response 956 | practice 957 | hardware 958 | figure 959 | materials 960 | fire 961 | holiday 962 | chat 963 | enough 964 | designed 965 | along 966 | among 967 | death 968 | writing 969 | speed 970 | html 971 | countries 972 | loss 973 | face 974 | brand 975 | discount 976 | higher 977 | effects 978 | created 979 | remember 980 | standards 981 | oil 982 | bit 983 | yellow 984 | political 985 | increase 986 | advertise 987 | kingdom 988 | base 989 | near 990 | environmental 991 | thought 992 | stuff 993 | french 994 | storage 995 | oh 996 | japan 997 | doing 998 | loans 999 | shoes 1000 | entry 1001 | -------------------------------------------------------------------------------- /dicts/1k_cap.txt: -------------------------------------------------------------------------------- 1 | The 2 | Of 3 | And 4 | To 5 | A 6 | In 7 | For 8 | Is 9 | On 10 | That 11 | By 12 | This 13 | With 14 | I 15 | You 16 | It 17 | Not 18 | Or 19 | Be 20 | Are 21 | From 22 | At 23 | As 24 | Your 25 | All 26 | Have 27 | New 28 | More 29 | An 30 | Was 31 | We 32 | Will 33 | Home 34 | Can 35 | Us 36 | About 37 | If 38 | Page 39 | My 40 | Has 41 | Search 42 | Free 43 | But 44 | Our 45 | One 46 | Other 47 | Do 48 | No 49 | Information 50 | Time 51 | They 52 | Site 53 | He 54 | Up 55 | May 56 | What 57 | Which 58 | Their 59 | News 60 | Out 61 | Use 62 | Any 63 | There 64 | See 65 | Only 66 | So 67 | His 68 | When 69 | Contact 70 | Here 71 | Business 72 | Who 73 | Web 74 | Also 75 | Now 76 | Help 77 | Get 78 | Pm 79 | View 80 | Online 81 | C 82 | E 83 | First 84 | Am 85 | Been 86 | Would 87 | How 88 | Were 89 | Me 90 | S 91 | Services 92 | Some 93 | These 94 | Click 95 | Its 96 | Like 97 | Service 98 | X 99 | Than 100 | Find 101 | Price 102 | Date 103 | Back 104 | Top 105 | People 106 | Had 107 | List 108 | Name 109 | Just 110 | Over 111 | State 112 | Year 113 | Day 114 | Into 115 | Email 116 | Two 117 | Health 118 | N 119 | World 120 | Re 121 | Next 122 | Used 123 | Go 124 | B 125 | Work 126 | Last 127 | Most 128 | Products 129 | Music 130 | Buy 131 | Data 132 | Make 133 | Them 134 | Should 135 | Product 136 | System 137 | Post 138 | Her 139 | City 140 | T 141 | Add 142 | Policy 143 | Number 144 | Such 145 | Please 146 | Available 147 | Copyright 148 | Support 149 | Message 150 | After 151 | Best 152 | Software 153 | Then 154 | Jan 155 | Good 156 | Video 157 | Well 158 | D 159 | Where 160 | Info 161 | Rights 162 | Public 163 | Books 164 | High 165 | School 166 | Through 167 | M 168 | Each 169 | Links 170 | She 171 | Review 172 | Years 173 | Order 174 | Very 175 | Privacy 176 | Book 177 | Items 178 | Company 179 | R 180 | Read 181 | Group 182 | Sex 183 | Need 184 | Many 185 | User 186 | Said 187 | De 188 | Does 189 | Set 190 | Under 191 | General 192 | Research 193 | University 194 | January 195 | Mail 196 | Full 197 | Map 198 | Reviews 199 | Program 200 | Life 201 | Know 202 | Games 203 | Way 204 | Days 205 | Management 206 | P 207 | Part 208 | Could 209 | Great 210 | United 211 | Hotel 212 | Real 213 | F 214 | Item 215 | International 216 | Center 217 | Ebay 218 | Must 219 | Store 220 | Travel 221 | Comments 222 | Made 223 | Development 224 | Report 225 | Off 226 | Member 227 | Details 228 | Line 229 | Terms 230 | Before 231 | Hotels 232 | Did 233 | Send 234 | Right 235 | Type 236 | Because 237 | Local 238 | Those 239 | Using 240 | Results 241 | Office 242 | Education 243 | National 244 | Car 245 | Design 246 | Take 247 | Posted 248 | Internet 249 | Address 250 | Community 251 | Within 252 | States 253 | Area 254 | Want 255 | Phone 256 | Dvd 257 | Shipping 258 | Reserved 259 | Subject 260 | Between 261 | Forum 262 | Family 263 | L 264 | Long 265 | Based 266 | W 267 | Code 268 | Show 269 | O 270 | Even 271 | Black 272 | Check 273 | Special 274 | Prices 275 | Website 276 | Index 277 | Being 278 | Women 279 | Much 280 | Sign 281 | File 282 | Link 283 | Open 284 | Today 285 | Technology 286 | South 287 | Case 288 | Project 289 | Same 290 | Pages 291 | Uk 292 | Version 293 | Section 294 | Own 295 | Found 296 | Sports 297 | House 298 | Related 299 | Security 300 | Both 301 | G 302 | County 303 | American 304 | Photo 305 | Game 306 | Members 307 | Power 308 | While 309 | Care 310 | Network 311 | Down 312 | Computer 313 | Systems 314 | Three 315 | Total 316 | Place 317 | End 318 | Following 319 | Download 320 | H 321 | Him 322 | Without 323 | Per 324 | Access 325 | Think 326 | North 327 | Resources 328 | Current 329 | Posts 330 | Big 331 | Media 332 | Law 333 | Control 334 | Water 335 | History 336 | Pictures 337 | Size 338 | Art 339 | Personal 340 | Since 341 | Including 342 | Guide 343 | Shop 344 | Directory 345 | Board 346 | Location 347 | Change 348 | White 349 | Text 350 | Small 351 | Rating 352 | Rate 353 | Government 354 | Children 355 | During 356 | Usa 357 | Return 358 | Students 359 | V 360 | Shopping 361 | Account 362 | Times 363 | Sites 364 | Level 365 | Digital 366 | Profile 367 | Previous 368 | Form 369 | Events 370 | Love 371 | Old 372 | John 373 | Main 374 | Call 375 | Hours 376 | Image 377 | Department 378 | Title 379 | Description 380 | Non 381 | K 382 | Y 383 | Insurance 384 | Another 385 | Why 386 | Shall 387 | Property 388 | Class 389 | Cd 390 | Still 391 | Money 392 | Quality 393 | Every 394 | Listing 395 | Content 396 | Country 397 | Private 398 | Little 399 | Visit 400 | Save 401 | Tools 402 | Low 403 | Reply 404 | Customer 405 | December 406 | Compare 407 | Movies 408 | Include 409 | College 410 | Value 411 | Article 412 | York 413 | Man 414 | Card 415 | Jobs 416 | Provide 417 | J 418 | Food 419 | Source 420 | Author 421 | Different 422 | Press 423 | U 424 | Learn 425 | Sale 426 | Around 427 | Print 428 | Course 429 | Job 430 | Canada 431 | Process 432 | Teen 433 | Room 434 | Stock 435 | Training 436 | Too 437 | Credit 438 | Point 439 | Join 440 | Science 441 | Men 442 | Categories 443 | Advanced 444 | West 445 | Sales 446 | Look 447 | English 448 | Left 449 | Team 450 | Estate 451 | Box 452 | Conditions 453 | Select 454 | Windows 455 | Photos 456 | Gay 457 | Thread 458 | Week 459 | Category 460 | Note 461 | Live 462 | Large 463 | Gallery 464 | Table 465 | Register 466 | However 467 | June 468 | October 469 | November 470 | Market 471 | Library 472 | Really 473 | Action 474 | Start 475 | Series 476 | Model 477 | Features 478 | Air 479 | Industry 480 | Plan 481 | Human 482 | Provided 483 | Tv 484 | Yes 485 | Required 486 | Second 487 | Hot 488 | Accessories 489 | Cost 490 | Movie 491 | Forums 492 | March 493 | La 494 | September 495 | Better 496 | Say 497 | Questions 498 | July 499 | Yahoo 500 | Going 501 | Medical 502 | Test 503 | Friend 504 | Come 505 | Dec 506 | Server 507 | Pc 508 | Study 509 | Application 510 | Cart 511 | Staff 512 | Articles 513 | San 514 | Feedback 515 | Again 516 | Play 517 | Looking 518 | Issues 519 | April 520 | Never 521 | Users 522 | Complete 523 | Street 524 | Topic 525 | Comment 526 | Financial 527 | Things 528 | Working 529 | Against 530 | Standard 531 | Tax 532 | Person 533 | Below 534 | Mobile 535 | Less 536 | Got 537 | Blog 538 | Party 539 | Payment 540 | Equipment 541 | Login 542 | Student 543 | Let 544 | Programs 545 | Offers 546 | Legal 547 | Above 548 | Recent 549 | Park 550 | Stores 551 | Side 552 | Act 553 | Problem 554 | Red 555 | Give 556 | Memory 557 | Performance 558 | Social 559 | Q 560 | August 561 | Quote 562 | Language 563 | Story 564 | Sell 565 | Options 566 | Experience 567 | Rates 568 | Create 569 | Key 570 | Body 571 | Young 572 | America 573 | Important 574 | Field 575 | Few 576 | East 577 | Paper 578 | Single 579 | Ii 580 | Age 581 | Activities 582 | Club 583 | Example 584 | Girls 585 | Additional 586 | Password 587 | Z 588 | Latest 589 | Something 590 | Road 591 | Gift 592 | Question 593 | Changes 594 | Night 595 | Ca 596 | Hard 597 | Texas 598 | Oct 599 | Pay 600 | Four 601 | Poker 602 | Status 603 | Browse 604 | Issue 605 | Range 606 | Building 607 | Seller 608 | Court 609 | February 610 | Always 611 | Result 612 | Audio 613 | Light 614 | Write 615 | War 616 | Nov 617 | Offer 618 | Blue 619 | Groups 620 | Al 621 | Easy 622 | Given 623 | Files 624 | Event 625 | Release 626 | Analysis 627 | Request 628 | Fax 629 | China 630 | Making 631 | Picture 632 | Needs 633 | Possible 634 | Might 635 | Professional 636 | Yet 637 | Month 638 | Major 639 | Star 640 | Areas 641 | Future 642 | Space 643 | Committee 644 | Hand 645 | Sun 646 | Cards 647 | Problems 648 | London 649 | Washington 650 | Meeting 651 | Rss 652 | Become 653 | Interest 654 | Id 655 | Child 656 | Keep 657 | Enter 658 | California 659 | Porn 660 | Share 661 | Similar 662 | Garden 663 | Schools 664 | Million 665 | Added 666 | Reference 667 | Companies 668 | Listed 669 | Baby 670 | Learning 671 | Energy 672 | Run 673 | Delivery 674 | Net 675 | Popular 676 | Term 677 | Film 678 | Stories 679 | Put 680 | Computers 681 | Journal 682 | Reports 683 | Co 684 | Try 685 | Welcome 686 | Central 687 | Images 688 | President 689 | Notice 690 | God 691 | Original 692 | Head 693 | Radio 694 | Until 695 | Cell 696 | Color 697 | Self 698 | Council 699 | Away 700 | Includes 701 | Track 702 | Australia 703 | Discussion 704 | Archive 705 | Once 706 | Others 707 | Entertainment 708 | Agreement 709 | Format 710 | Least 711 | Society 712 | Months 713 | Log 714 | Safety 715 | Friends 716 | Sure 717 | Faq 718 | Trade 719 | Edition 720 | Cars 721 | Messages 722 | Marketing 723 | Tell 724 | Further 725 | Updated 726 | Association 727 | Able 728 | Having 729 | Provides 730 | David 731 | Fun 732 | Already 733 | Green 734 | Studies 735 | Close 736 | Common 737 | Drive 738 | Specific 739 | Several 740 | Gold 741 | Feb 742 | Living 743 | Sep 744 | Collection 745 | Called 746 | Short 747 | Arts 748 | Lot 749 | Ask 750 | Display 751 | Limited 752 | Powered 753 | Solutions 754 | Means 755 | Director 756 | Daily 757 | Beach 758 | Past 759 | Natural 760 | Whether 761 | Due 762 | Et 763 | Electronics 764 | Five 765 | Upon 766 | Period 767 | Planning 768 | Database 769 | Says 770 | Official 771 | Weather 772 | Mar 773 | Land 774 | Average 775 | Done 776 | Technical 777 | Window 778 | France 779 | Pro 780 | Region 781 | Island 782 | Record 783 | Direct 784 | Microsoft 785 | Conference 786 | Environment 787 | Records 788 | St 789 | District 790 | Calendar 791 | Costs 792 | Style 793 | Url 794 | Front 795 | Statement 796 | Update 797 | Parts 798 | Aug 799 | Ever 800 | Downloads 801 | Early 802 | Miles 803 | Sound 804 | Resource 805 | Present 806 | Applications 807 | Either 808 | Ago 809 | Document 810 | Word 811 | Works 812 | Material 813 | Bill 814 | Apr 815 | Written 816 | Talk 817 | Federal 818 | Hosting 819 | Rules 820 | Final 821 | Adult 822 | Tickets 823 | Thing 824 | Centre 825 | Requirements 826 | Via 827 | Cheap 828 | Nude 829 | Kids 830 | Finance 831 | True 832 | Minutes 833 | Else 834 | Mark 835 | Third 836 | Rock 837 | Gifts 838 | Europe 839 | Reading 840 | Topics 841 | Bad 842 | Individual 843 | Tips 844 | Plus 845 | Auto 846 | Cover 847 | Usually 848 | Edit 849 | Together 850 | Videos 851 | Percent 852 | Fast 853 | Function 854 | Fact 855 | Unit 856 | Getting 857 | Global 858 | Tech 859 | Meet 860 | Far 861 | Economic 862 | En 863 | Player 864 | Projects 865 | Lyrics 866 | Often 867 | Subscribe 868 | Submit 869 | Germany 870 | Amount 871 | Watch 872 | Included 873 | Feel 874 | Though 875 | Bank 876 | Risk 877 | Thanks 878 | Everything 879 | Deals 880 | Various 881 | Words 882 | Linux 883 | Jul 884 | Production 885 | Commercial 886 | James 887 | Weight 888 | Town 889 | Heart 890 | Advertising 891 | Received 892 | Choose 893 | Treatment 894 | Newsletter 895 | Archives 896 | Points 897 | Knowledge 898 | Magazine 899 | Error 900 | Camera 901 | Jun 902 | Girl 903 | Currently 904 | Construction 905 | Toys 906 | Registered 907 | Clear 908 | Golf 909 | Receive 910 | Domain 911 | Methods 912 | Chapter 913 | Makes 914 | Protection 915 | Policies 916 | Loan 917 | Wide 918 | Beauty 919 | Manager 920 | India 921 | Position 922 | Taken 923 | Sort 924 | Listings 925 | Models 926 | Michael 927 | Known 928 | Half 929 | Cases 930 | Step 931 | Engineering 932 | Florida 933 | Simple 934 | Quick 935 | None 936 | Wireless 937 | License 938 | Paul 939 | Friday 940 | Lake 941 | Whole 942 | Annual 943 | Published 944 | Later 945 | Basic 946 | Sony 947 | Shows 948 | Corporate 949 | Google 950 | Church 951 | Method 952 | Purchase 953 | Customers 954 | Active 955 | Response 956 | Practice 957 | Hardware 958 | Figure 959 | Materials 960 | Fire 961 | Holiday 962 | Chat 963 | Enough 964 | Designed 965 | Along 966 | Among 967 | Death 968 | Writing 969 | Speed 970 | Html 971 | Countries 972 | Loss 973 | Face 974 | Brand 975 | Discount 976 | Higher 977 | Effects 978 | Created 979 | Remember 980 | Standards 981 | Oil 982 | Bit 983 | Yellow 984 | Political 985 | Increase 986 | Advertise 987 | Kingdom 988 | Base 989 | Near 990 | Environmental 991 | Thought 992 | Stuff 993 | French 994 | Storage 995 | Oh 996 | Japan 997 | Doing 998 | Loans 999 | Shoes 1000 | Entry 1001 | -------------------------------------------------------------------------------- /dicts/1k_upper.txt: -------------------------------------------------------------------------------- 1 | THE 2 | OF 3 | AND 4 | TO 5 | A 6 | IN 7 | FOR 8 | IS 9 | ON 10 | THAT 11 | BY 12 | THIS 13 | WITH 14 | I 15 | YOU 16 | IT 17 | NOT 18 | OR 19 | BE 20 | ARE 21 | FROM 22 | AT 23 | AS 24 | YOUR 25 | ALL 26 | HAVE 27 | NEW 28 | MORE 29 | AN 30 | WAS 31 | WE 32 | WILL 33 | HOME 34 | CAN 35 | US 36 | ABOUT 37 | IF 38 | PAGE 39 | MY 40 | HAS 41 | SEARCH 42 | FREE 43 | BUT 44 | OUR 45 | ONE 46 | OTHER 47 | DO 48 | NO 49 | INFORMATION 50 | TIME 51 | THEY 52 | SITE 53 | HE 54 | UP 55 | MAY 56 | WHAT 57 | WHICH 58 | THEIR 59 | NEWS 60 | OUT 61 | USE 62 | ANY 63 | THERE 64 | SEE 65 | ONLY 66 | SO 67 | HIS 68 | WHEN 69 | CONTACT 70 | HERE 71 | BUSINESS 72 | WHO 73 | WEB 74 | ALSO 75 | NOW 76 | HELP 77 | GET 78 | PM 79 | VIEW 80 | ONLINE 81 | C 82 | E 83 | FIRST 84 | AM 85 | BEEN 86 | WOULD 87 | HOW 88 | WERE 89 | ME 90 | S 91 | SERVICES 92 | SOME 93 | THESE 94 | CLICK 95 | ITS 96 | LIKE 97 | SERVICE 98 | X 99 | THAN 100 | FIND 101 | PRICE 102 | DATE 103 | BACK 104 | TOP 105 | PEOPLE 106 | HAD 107 | LIST 108 | NAME 109 | JUST 110 | OVER 111 | STATE 112 | YEAR 113 | DAY 114 | INTO 115 | EMAIL 116 | TWO 117 | HEALTH 118 | N 119 | WORLD 120 | RE 121 | NEXT 122 | USED 123 | GO 124 | B 125 | WORK 126 | LAST 127 | MOST 128 | PRODUCTS 129 | MUSIC 130 | BUY 131 | DATA 132 | MAKE 133 | THEM 134 | SHOULD 135 | PRODUCT 136 | SYSTEM 137 | POST 138 | HER 139 | CITY 140 | T 141 | ADD 142 | POLICY 143 | NUMBER 144 | SUCH 145 | PLEASE 146 | AVAILABLE 147 | COPYRIGHT 148 | SUPPORT 149 | MESSAGE 150 | AFTER 151 | BEST 152 | SOFTWARE 153 | THEN 154 | JAN 155 | GOOD 156 | VIDEO 157 | WELL 158 | D 159 | WHERE 160 | INFO 161 | RIGHTS 162 | PUBLIC 163 | BOOKS 164 | HIGH 165 | SCHOOL 166 | THROUGH 167 | M 168 | EACH 169 | LINKS 170 | SHE 171 | REVIEW 172 | YEARS 173 | ORDER 174 | VERY 175 | PRIVACY 176 | BOOK 177 | ITEMS 178 | COMPANY 179 | R 180 | READ 181 | GROUP 182 | SEX 183 | NEED 184 | MANY 185 | USER 186 | SAID 187 | DE 188 | DOES 189 | SET 190 | UNDER 191 | GENERAL 192 | RESEARCH 193 | UNIVERSITY 194 | JANUARY 195 | MAIL 196 | FULL 197 | MAP 198 | REVIEWS 199 | PROGRAM 200 | LIFE 201 | KNOW 202 | GAMES 203 | WAY 204 | DAYS 205 | MANAGEMENT 206 | P 207 | PART 208 | COULD 209 | GREAT 210 | UNITED 211 | HOTEL 212 | REAL 213 | F 214 | ITEM 215 | INTERNATIONAL 216 | CENTER 217 | EBAY 218 | MUST 219 | STORE 220 | TRAVEL 221 | COMMENTS 222 | MADE 223 | DEVELOPMENT 224 | REPORT 225 | OFF 226 | MEMBER 227 | DETAILS 228 | LINE 229 | TERMS 230 | BEFORE 231 | HOTELS 232 | DID 233 | SEND 234 | RIGHT 235 | TYPE 236 | BECAUSE 237 | LOCAL 238 | THOSE 239 | USING 240 | RESULTS 241 | OFFICE 242 | EDUCATION 243 | NATIONAL 244 | CAR 245 | DESIGN 246 | TAKE 247 | POSTED 248 | INTERNET 249 | ADDRESS 250 | COMMUNITY 251 | WITHIN 252 | STATES 253 | AREA 254 | WANT 255 | PHONE 256 | DVD 257 | SHIPPING 258 | RESERVED 259 | SUBJECT 260 | BETWEEN 261 | FORUM 262 | FAMILY 263 | L 264 | LONG 265 | BASED 266 | W 267 | CODE 268 | SHOW 269 | O 270 | EVEN 271 | BLACK 272 | CHECK 273 | SPECIAL 274 | PRICES 275 | WEBSITE 276 | INDEX 277 | BEING 278 | WOMEN 279 | MUCH 280 | SIGN 281 | FILE 282 | LINK 283 | OPEN 284 | TODAY 285 | TECHNOLOGY 286 | SOUTH 287 | CASE 288 | PROJECT 289 | SAME 290 | PAGES 291 | UK 292 | VERSION 293 | SECTION 294 | OWN 295 | FOUND 296 | SPORTS 297 | HOUSE 298 | RELATED 299 | SECURITY 300 | BOTH 301 | G 302 | COUNTY 303 | AMERICAN 304 | PHOTO 305 | GAME 306 | MEMBERS 307 | POWER 308 | WHILE 309 | CARE 310 | NETWORK 311 | DOWN 312 | COMPUTER 313 | SYSTEMS 314 | THREE 315 | TOTAL 316 | PLACE 317 | END 318 | FOLLOWING 319 | DOWNLOAD 320 | H 321 | HIM 322 | WITHOUT 323 | PER 324 | ACCESS 325 | THINK 326 | NORTH 327 | RESOURCES 328 | CURRENT 329 | POSTS 330 | BIG 331 | MEDIA 332 | LAW 333 | CONTROL 334 | WATER 335 | HISTORY 336 | PICTURES 337 | SIZE 338 | ART 339 | PERSONAL 340 | SINCE 341 | INCLUDING 342 | GUIDE 343 | SHOP 344 | DIRECTORY 345 | BOARD 346 | LOCATION 347 | CHANGE 348 | WHITE 349 | TEXT 350 | SMALL 351 | RATING 352 | RATE 353 | GOVERNMENT 354 | CHILDREN 355 | DURING 356 | USA 357 | RETURN 358 | STUDENTS 359 | V 360 | SHOPPING 361 | ACCOUNT 362 | TIMES 363 | SITES 364 | LEVEL 365 | DIGITAL 366 | PROFILE 367 | PREVIOUS 368 | FORM 369 | EVENTS 370 | LOVE 371 | OLD 372 | JOHN 373 | MAIN 374 | CALL 375 | HOURS 376 | IMAGE 377 | DEPARTMENT 378 | TITLE 379 | DESCRIPTION 380 | NON 381 | K 382 | Y 383 | INSURANCE 384 | ANOTHER 385 | WHY 386 | SHALL 387 | PROPERTY 388 | CLASS 389 | CD 390 | STILL 391 | MONEY 392 | QUALITY 393 | EVERY 394 | LISTING 395 | CONTENT 396 | COUNTRY 397 | PRIVATE 398 | LITTLE 399 | VISIT 400 | SAVE 401 | TOOLS 402 | LOW 403 | REPLY 404 | CUSTOMER 405 | DECEMBER 406 | COMPARE 407 | MOVIES 408 | INCLUDE 409 | COLLEGE 410 | VALUE 411 | ARTICLE 412 | YORK 413 | MAN 414 | CARD 415 | JOBS 416 | PROVIDE 417 | J 418 | FOOD 419 | SOURCE 420 | AUTHOR 421 | DIFFERENT 422 | PRESS 423 | U 424 | LEARN 425 | SALE 426 | AROUND 427 | PRINT 428 | COURSE 429 | JOB 430 | CANADA 431 | PROCESS 432 | TEEN 433 | ROOM 434 | STOCK 435 | TRAINING 436 | TOO 437 | CREDIT 438 | POINT 439 | JOIN 440 | SCIENCE 441 | MEN 442 | CATEGORIES 443 | ADVANCED 444 | WEST 445 | SALES 446 | LOOK 447 | ENGLISH 448 | LEFT 449 | TEAM 450 | ESTATE 451 | BOX 452 | CONDITIONS 453 | SELECT 454 | WINDOWS 455 | PHOTOS 456 | GAY 457 | THREAD 458 | WEEK 459 | CATEGORY 460 | NOTE 461 | LIVE 462 | LARGE 463 | GALLERY 464 | TABLE 465 | REGISTER 466 | HOWEVER 467 | JUNE 468 | OCTOBER 469 | NOVEMBER 470 | MARKET 471 | LIBRARY 472 | REALLY 473 | ACTION 474 | START 475 | SERIES 476 | MODEL 477 | FEATURES 478 | AIR 479 | INDUSTRY 480 | PLAN 481 | HUMAN 482 | PROVIDED 483 | TV 484 | YES 485 | REQUIRED 486 | SECOND 487 | HOT 488 | ACCESSORIES 489 | COST 490 | MOVIE 491 | FORUMS 492 | MARCH 493 | LA 494 | SEPTEMBER 495 | BETTER 496 | SAY 497 | QUESTIONS 498 | JULY 499 | YAHOO 500 | GOING 501 | MEDICAL 502 | TEST 503 | FRIEND 504 | COME 505 | DEC 506 | SERVER 507 | PC 508 | STUDY 509 | APPLICATION 510 | CART 511 | STAFF 512 | ARTICLES 513 | SAN 514 | FEEDBACK 515 | AGAIN 516 | PLAY 517 | LOOKING 518 | ISSUES 519 | APRIL 520 | NEVER 521 | USERS 522 | COMPLETE 523 | STREET 524 | TOPIC 525 | COMMENT 526 | FINANCIAL 527 | THINGS 528 | WORKING 529 | AGAINST 530 | STANDARD 531 | TAX 532 | PERSON 533 | BELOW 534 | MOBILE 535 | LESS 536 | GOT 537 | BLOG 538 | PARTY 539 | PAYMENT 540 | EQUIPMENT 541 | LOGIN 542 | STUDENT 543 | LET 544 | PROGRAMS 545 | OFFERS 546 | LEGAL 547 | ABOVE 548 | RECENT 549 | PARK 550 | STORES 551 | SIDE 552 | ACT 553 | PROBLEM 554 | RED 555 | GIVE 556 | MEMORY 557 | PERFORMANCE 558 | SOCIAL 559 | Q 560 | AUGUST 561 | QUOTE 562 | LANGUAGE 563 | STORY 564 | SELL 565 | OPTIONS 566 | EXPERIENCE 567 | RATES 568 | CREATE 569 | KEY 570 | BODY 571 | YOUNG 572 | AMERICA 573 | IMPORTANT 574 | FIELD 575 | FEW 576 | EAST 577 | PAPER 578 | SINGLE 579 | II 580 | AGE 581 | ACTIVITIES 582 | CLUB 583 | EXAMPLE 584 | GIRLS 585 | ADDITIONAL 586 | PASSWORD 587 | Z 588 | LATEST 589 | SOMETHING 590 | ROAD 591 | GIFT 592 | QUESTION 593 | CHANGES 594 | NIGHT 595 | CA 596 | HARD 597 | TEXAS 598 | OCT 599 | PAY 600 | FOUR 601 | POKER 602 | STATUS 603 | BROWSE 604 | ISSUE 605 | RANGE 606 | BUILDING 607 | SELLER 608 | COURT 609 | FEBRUARY 610 | ALWAYS 611 | RESULT 612 | AUDIO 613 | LIGHT 614 | WRITE 615 | WAR 616 | NOV 617 | OFFER 618 | BLUE 619 | GROUPS 620 | AL 621 | EASY 622 | GIVEN 623 | FILES 624 | EVENT 625 | RELEASE 626 | ANALYSIS 627 | REQUEST 628 | FAX 629 | CHINA 630 | MAKING 631 | PICTURE 632 | NEEDS 633 | POSSIBLE 634 | MIGHT 635 | PROFESSIONAL 636 | YET 637 | MONTH 638 | MAJOR 639 | STAR 640 | AREAS 641 | FUTURE 642 | SPACE 643 | COMMITTEE 644 | HAND 645 | SUN 646 | CARDS 647 | PROBLEMS 648 | LONDON 649 | WASHINGTON 650 | MEETING 651 | RSS 652 | BECOME 653 | INTEREST 654 | ID 655 | CHILD 656 | KEEP 657 | ENTER 658 | CALIFORNIA 659 | PORN 660 | SHARE 661 | SIMILAR 662 | GARDEN 663 | SCHOOLS 664 | MILLION 665 | ADDED 666 | REFERENCE 667 | COMPANIES 668 | LISTED 669 | BABY 670 | LEARNING 671 | ENERGY 672 | RUN 673 | DELIVERY 674 | NET 675 | POPULAR 676 | TERM 677 | FILM 678 | STORIES 679 | PUT 680 | COMPUTERS 681 | JOURNAL 682 | REPORTS 683 | CO 684 | TRY 685 | WELCOME 686 | CENTRAL 687 | IMAGES 688 | PRESIDENT 689 | NOTICE 690 | GOD 691 | ORIGINAL 692 | HEAD 693 | RADIO 694 | UNTIL 695 | CELL 696 | COLOR 697 | SELF 698 | COUNCIL 699 | AWAY 700 | INCLUDES 701 | TRACK 702 | AUSTRALIA 703 | DISCUSSION 704 | ARCHIVE 705 | ONCE 706 | OTHERS 707 | ENTERTAINMENT 708 | AGREEMENT 709 | FORMAT 710 | LEAST 711 | SOCIETY 712 | MONTHS 713 | LOG 714 | SAFETY 715 | FRIENDS 716 | SURE 717 | FAQ 718 | TRADE 719 | EDITION 720 | CARS 721 | MESSAGES 722 | MARKETING 723 | TELL 724 | FURTHER 725 | UPDATED 726 | ASSOCIATION 727 | ABLE 728 | HAVING 729 | PROVIDES 730 | DAVID 731 | FUN 732 | ALREADY 733 | GREEN 734 | STUDIES 735 | CLOSE 736 | COMMON 737 | DRIVE 738 | SPECIFIC 739 | SEVERAL 740 | GOLD 741 | FEB 742 | LIVING 743 | SEP 744 | COLLECTION 745 | CALLED 746 | SHORT 747 | ARTS 748 | LOT 749 | ASK 750 | DISPLAY 751 | LIMITED 752 | POWERED 753 | SOLUTIONS 754 | MEANS 755 | DIRECTOR 756 | DAILY 757 | BEACH 758 | PAST 759 | NATURAL 760 | WHETHER 761 | DUE 762 | ET 763 | ELECTRONICS 764 | FIVE 765 | UPON 766 | PERIOD 767 | PLANNING 768 | DATABASE 769 | SAYS 770 | OFFICIAL 771 | WEATHER 772 | MAR 773 | LAND 774 | AVERAGE 775 | DONE 776 | TECHNICAL 777 | WINDOW 778 | FRANCE 779 | PRO 780 | REGION 781 | ISLAND 782 | RECORD 783 | DIRECT 784 | MICROSOFT 785 | CONFERENCE 786 | ENVIRONMENT 787 | RECORDS 788 | ST 789 | DISTRICT 790 | CALENDAR 791 | COSTS 792 | STYLE 793 | URL 794 | FRONT 795 | STATEMENT 796 | UPDATE 797 | PARTS 798 | AUG 799 | EVER 800 | DOWNLOADS 801 | EARLY 802 | MILES 803 | SOUND 804 | RESOURCE 805 | PRESENT 806 | APPLICATIONS 807 | EITHER 808 | AGO 809 | DOCUMENT 810 | WORD 811 | WORKS 812 | MATERIAL 813 | BILL 814 | APR 815 | WRITTEN 816 | TALK 817 | FEDERAL 818 | HOSTING 819 | RULES 820 | FINAL 821 | ADULT 822 | TICKETS 823 | THING 824 | CENTRE 825 | REQUIREMENTS 826 | VIA 827 | CHEAP 828 | NUDE 829 | KIDS 830 | FINANCE 831 | TRUE 832 | MINUTES 833 | ELSE 834 | MARK 835 | THIRD 836 | ROCK 837 | GIFTS 838 | EUROPE 839 | READING 840 | TOPICS 841 | BAD 842 | INDIVIDUAL 843 | TIPS 844 | PLUS 845 | AUTO 846 | COVER 847 | USUALLY 848 | EDIT 849 | TOGETHER 850 | VIDEOS 851 | PERCENT 852 | FAST 853 | FUNCTION 854 | FACT 855 | UNIT 856 | GETTING 857 | GLOBAL 858 | TECH 859 | MEET 860 | FAR 861 | ECONOMIC 862 | EN 863 | PLAYER 864 | PROJECTS 865 | LYRICS 866 | OFTEN 867 | SUBSCRIBE 868 | SUBMIT 869 | GERMANY 870 | AMOUNT 871 | WATCH 872 | INCLUDED 873 | FEEL 874 | THOUGH 875 | BANK 876 | RISK 877 | THANKS 878 | EVERYTHING 879 | DEALS 880 | VARIOUS 881 | WORDS 882 | LINUX 883 | JUL 884 | PRODUCTION 885 | COMMERCIAL 886 | JAMES 887 | WEIGHT 888 | TOWN 889 | HEART 890 | ADVERTISING 891 | RECEIVED 892 | CHOOSE 893 | TREATMENT 894 | NEWSLETTER 895 | ARCHIVES 896 | POINTS 897 | KNOWLEDGE 898 | MAGAZINE 899 | ERROR 900 | CAMERA 901 | JUN 902 | GIRL 903 | CURRENTLY 904 | CONSTRUCTION 905 | TOYS 906 | REGISTERED 907 | CLEAR 908 | GOLF 909 | RECEIVE 910 | DOMAIN 911 | METHODS 912 | CHAPTER 913 | MAKES 914 | PROTECTION 915 | POLICIES 916 | LOAN 917 | WIDE 918 | BEAUTY 919 | MANAGER 920 | INDIA 921 | POSITION 922 | TAKEN 923 | SORT 924 | LISTINGS 925 | MODELS 926 | MICHAEL 927 | KNOWN 928 | HALF 929 | CASES 930 | STEP 931 | ENGINEERING 932 | FLORIDA 933 | SIMPLE 934 | QUICK 935 | NONE 936 | WIRELESS 937 | LICENSE 938 | PAUL 939 | FRIDAY 940 | LAKE 941 | WHOLE 942 | ANNUAL 943 | PUBLISHED 944 | LATER 945 | BASIC 946 | SONY 947 | SHOWS 948 | CORPORATE 949 | GOOGLE 950 | CHURCH 951 | METHOD 952 | PURCHASE 953 | CUSTOMERS 954 | ACTIVE 955 | RESPONSE 956 | PRACTICE 957 | HARDWARE 958 | FIGURE 959 | MATERIALS 960 | FIRE 961 | HOLIDAY 962 | CHAT 963 | ENOUGH 964 | DESIGNED 965 | ALONG 966 | AMONG 967 | DEATH 968 | WRITING 969 | SPEED 970 | HTML 971 | COUNTRIES 972 | LOSS 973 | FACE 974 | BRAND 975 | DISCOUNT 976 | HIGHER 977 | EFFECTS 978 | CREATED 979 | REMEMBER 980 | STANDARDS 981 | OIL 982 | BIT 983 | YELLOW 984 | POLITICAL 985 | INCREASE 986 | ADVERTISE 987 | KINGDOM 988 | BASE 989 | NEAR 990 | ENVIRONMENTAL 991 | THOUGHT 992 | STUFF 993 | FRENCH 994 | STORAGE 995 | OH 996 | JAPAN 997 | DOING 998 | LOANS 999 | SHOES 1000 | ENTRY 1001 | -------------------------------------------------------------------------------- /src/logger.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::max; 2 | use std::io::{stdout, Write}; 3 | use std::ops::Sub; 4 | use std::sync::atomic::{AtomicU64, Ordering}; 5 | use std::sync::Arc; 6 | use std::time::{Duration, Instant}; 7 | 8 | use anyhow::{bail, Result}; 9 | use crossterm::cursor::*; 10 | use crossterm::style::StyledContent; 11 | use crossterm::style::Stylize; 12 | use crossterm::terminal::{Clear, ClearType}; 13 | use crossterm::ExecutableCommand; 14 | use tokio::spawn; 15 | use tokio::task::JoinHandle; 16 | use tokio::time::sleep; 17 | 18 | /// Trait that can be logged for configuration purposes 19 | pub trait Attempt { 20 | fn total(&self) -> u64; 21 | fn begin(&self) -> String; 22 | fn end(&self) -> String; 23 | } 24 | 25 | const MINUTE: u64 = 60; 26 | const HOUR: u64 = MINUTE * 60; 27 | const DAY: u64 = HOUR * 24; 28 | 29 | /// Logger that can be either off or on 30 | #[derive(Debug, Clone, Eq, PartialEq)] 31 | pub struct Logger { 32 | is_logging: bool, 33 | } 34 | 35 | /// Formats table headings and rows 36 | pub struct TableFormat { 37 | headings: Vec, 38 | log: Logger, 39 | } 40 | 41 | impl TableFormat { 42 | /// Logs the table heading 43 | pub fn log_heading(&self) { 44 | let headings = self.headings.join("|"); 45 | self.log.println(headings.as_str().bold()); 46 | } 47 | 48 | fn format(&self, row: Vec) -> String { 49 | let mut padded = vec![]; 50 | for i in 0..row.len() { 51 | let len = self.headings[i].len(); 52 | padded.push(format!("{: <1$}", row[i], len)); 53 | } 54 | padded.join("|") 55 | } 56 | 57 | /// Logs the row containing same number of strings as heading 58 | pub fn log_row(&self, row: Vec) { 59 | self.log.println(self.format(row).as_str().stylize()); 60 | } 61 | } 62 | 63 | /// Periodically logs the time and progress of a task 64 | #[derive(Debug, Clone)] 65 | pub struct Timer { 66 | name: String, 67 | oneliner: bool, 68 | total: Arc, 69 | end: Arc, 70 | counter: Arc, 71 | seconds: Arc, 72 | last_speed: Arc, 73 | multiplier: u64, 74 | log: Logger, 75 | } 76 | 77 | impl Timer { 78 | /// Get the number of seconds elapsed 79 | pub fn seconds(&self) -> u64 { 80 | max(self.seconds.fetch_add(0, Ordering::Relaxed), 1) 81 | } 82 | 83 | /// Get the current count 84 | pub fn count(&self) -> u64 { 85 | self.counter 86 | .fetch_add(0, Ordering::Relaxed) 87 | .saturating_mul(self.multiplier) 88 | } 89 | 90 | /// Get the speed string without the multiplier 91 | pub fn gpu_speed(&self) -> String { 92 | Logger::format_num(self.count() / self.seconds() / self.multiplier) 93 | } 94 | 95 | /// Get the speed string 96 | pub fn speed(&self) -> String { 97 | Logger::format_num(self.count() / self.seconds()) 98 | } 99 | 100 | /// Add to the count 101 | pub fn add(&self, amt: u64) { 102 | self.counter.fetch_add(amt, Ordering::Relaxed); 103 | } 104 | 105 | /// Store the count 106 | pub fn store(&self, amt: u64) { 107 | self.counter.store(amt, Ordering::Relaxed); 108 | } 109 | 110 | /// Tell the timer loop to end 111 | pub fn end(&self) { 112 | self.end.store(1, Ordering::Relaxed); 113 | } 114 | 115 | /// Start the timer 116 | pub async fn start(&self) -> JoinHandle<()> { 117 | self.start_at(0).await 118 | } 119 | 120 | /// Start the timer with secs already elapsed 121 | pub async fn start_at(&self, secs: u64) -> JoinHandle<()> { 122 | let timer = self.clone(); 123 | 124 | spawn(async move { 125 | let now = Instant::now().sub(Duration::from_secs(secs)); 126 | let mut old_count = u64::MAX; 127 | let name = timer.name.as_str().bold(); 128 | 129 | loop { 130 | sleep(Duration::from_millis(100)).await; 131 | let count = timer.count(); 132 | let end = timer.end.fetch_add(0, Ordering::Relaxed); 133 | 134 | // Don't print if the count hasn't changed 135 | if count == old_count && end == 0 { 136 | continue; 137 | } 138 | let total = timer.total.fetch_add(0, Ordering::Relaxed); 139 | 140 | if !timer.oneliner && old_count == u64::MAX { 141 | timer.log.println("\n\n\n\n\n".stylize()); 142 | } 143 | 144 | timer 145 | .seconds 146 | .store(now.elapsed().as_secs(), Ordering::Relaxed); 147 | let seconds = timer.seconds(); 148 | timer 149 | .last_speed 150 | .store(old_count / seconds, Ordering::Relaxed); 151 | old_count = count; 152 | 153 | let mut percent = (count as f64 / total as f64) * 100.0; 154 | if percent > 100.0 { 155 | percent = 100.0; 156 | } 157 | let count_str = Logger::format_num(count); 158 | let total_str = Logger::format_num(total); 159 | let speed = format!("Speed....: {}/sec", timer.speed()); 160 | let gpu = format!("GPU Speed: {}/sec", timer.gpu_speed()); 161 | let progress = format!(" {:.2}% ({}/{})", percent, count_str, total_str); 162 | let eta = format!("ETA......: {}", Self::format_eta(percent, seconds)); 163 | let elapsed = format!("Elapsed..: {}", Self::format_time(seconds)); 164 | let output = format!( 165 | "\n Progress:{}\n {}\n {}\n {}\n {}", 166 | progress, speed, gpu, eta, elapsed 167 | ); 168 | 169 | let mut stdout = stdout(); 170 | if timer.log.is_logging && timer.oneliner { 171 | stdout.execute(MoveLeft(1000)).unwrap(); 172 | stdout.execute(Clear(ClearType::FromCursorDown)).unwrap(); 173 | stdout.write_all(name.to_string().as_bytes()).unwrap(); 174 | stdout.write_all(progress.to_string().as_bytes()).unwrap(); 175 | stdout.flush().unwrap(); 176 | } else if timer.log.is_logging { 177 | stdout.execute(MoveLeft(1000)).unwrap(); 178 | stdout.execute(MoveUp(6)).unwrap(); 179 | stdout.execute(Clear(ClearType::FromCursorDown)).unwrap(); 180 | stdout.write_all("\n".as_bytes()).unwrap(); 181 | stdout.write_all(name.to_string().as_bytes()).unwrap(); 182 | stdout.write_all(output.to_string().as_bytes()).unwrap(); 183 | stdout.flush().unwrap(); 184 | } 185 | if count >= total || end != 0 { 186 | timer.log.println("\n".stylize()); 187 | break; 188 | } 189 | } 190 | }) 191 | } 192 | 193 | fn format_eta(percent: f64, secs: u64) -> String { 194 | if percent == 100.0 { 195 | return "N/A".to_string(); 196 | } 197 | if percent.is_nan() || percent == 0.0 { 198 | return "Unknown".to_string(); 199 | } 200 | let remaining = (secs as f64 * (100.0 / percent)) as u64; 201 | if remaining <= secs { 202 | return "Unknown".to_string(); 203 | } 204 | Self::format_time(remaining - secs) 205 | } 206 | 207 | /// Formats time duration in seconds 208 | pub fn format_time(mut seconds: u64) -> String { 209 | let mut output = vec![]; 210 | 211 | if seconds / DAY > 0 { 212 | output.push(format!("{} days", seconds / DAY)); 213 | seconds %= DAY; 214 | } 215 | if seconds / HOUR > 0 || output.len() > 0 { 216 | output.push(format!("{} hours", seconds / HOUR)); 217 | seconds %= HOUR; 218 | } 219 | if seconds / MINUTE > 0 || output.len() > 0 { 220 | output.push(format!("{} mins", seconds / MINUTE)); 221 | seconds %= MINUTE; 222 | } 223 | output.push(format!("{} secs", seconds)); 224 | output.join(", ") 225 | } 226 | } 227 | 228 | impl Logger { 229 | /// Create logger that logs 230 | pub fn new() -> Self { 231 | Self { is_logging: true } 232 | } 233 | 234 | /// Create logger that doesn't log 235 | pub fn off() -> Self { 236 | Self { is_logging: false } 237 | } 238 | 239 | /// Create a new table logger, columns will be padded to heading length 240 | pub fn table(&self, heading: Vec<&str>) -> TableFormat { 241 | TableFormat { 242 | headings: heading.iter().map(|s| s.to_string()).collect(), 243 | log: self.clone(), 244 | } 245 | } 246 | 247 | /// Create a new timer logger 248 | pub async fn time(&self, name: &str, total: u64) -> Timer { 249 | Timer { 250 | name: name.to_string(), 251 | oneliner: true, 252 | total: Arc::new(AtomicU64::new(total)), 253 | end: Arc::new(Default::default()), 254 | counter: Arc::new(Default::default()), 255 | seconds: Arc::new(Default::default()), 256 | last_speed: Arc::new(Default::default()), 257 | multiplier: 1, 258 | log: self.clone(), 259 | } 260 | } 261 | 262 | /// Create a new timer logger in verbose mode 263 | /// `multiplier` will multiply the count (not the total) 264 | pub async fn time_verbose(&self, name: &str, total: u64, multiplier: u64) -> Timer { 265 | Timer { 266 | name: name.to_string(), 267 | oneliner: false, 268 | total: Arc::new(AtomicU64::new(total)), 269 | end: Arc::new(Default::default()), 270 | counter: Arc::new(Default::default()), 271 | seconds: Arc::new(Default::default()), 272 | last_speed: Arc::new(Default::default()), 273 | multiplier, 274 | log: self.clone(), 275 | } 276 | } 277 | 278 | /// Log a heading 279 | pub fn heading(&self, output: &str) { 280 | self.print( 281 | format!("\n============ {} ============\n", output) 282 | .as_str() 283 | .dark_green() 284 | .bold(), 285 | ) 286 | } 287 | 288 | /// Print stylized text 289 | pub fn print(&self, output: StyledContent<&str>) { 290 | let mut stdout = stdout(); 291 | if self.is_logging { 292 | stdout.write_all(output.to_string().as_bytes()).unwrap(); 293 | stdout.flush().unwrap(); 294 | } 295 | } 296 | 297 | /// Print error text 298 | pub fn println_err(&self, output: &str) { 299 | let mut split = output.split("\n"); 300 | self.print("\nError: ".dark_red().bold()); 301 | while let Some(line) = split.next() { 302 | self.println(line.stylize()); 303 | } 304 | self.println("\n If you found a bug please report it here: https://github.com/seed-cat/seedcat/issues".stylize()); 305 | } 306 | 307 | /// Println stylized text 308 | pub fn println(&self, output: StyledContent<&str>) { 309 | let mut stdout = stdout(); 310 | if self.is_logging { 311 | stdout.write_all(output.to_string().as_bytes()).unwrap(); 312 | stdout.write_all("\n".to_string().as_bytes()).unwrap(); 313 | stdout.flush().unwrap(); 314 | } 315 | } 316 | 317 | /// Log an Attempt (begin, end, total) 318 | pub fn format_attempt(&self, name: &str, attempt: &impl Attempt) { 319 | let total = format!("{}: ", name); 320 | self.print_num(&total, attempt.total()); 321 | self.println( 322 | format!(" Begin: {}\n End: {}\n", attempt.begin(), attempt.end()) 323 | .as_str() 324 | .stylize(), 325 | ); 326 | } 327 | 328 | /// Log a number 329 | pub fn print_num(&self, prefix: &str, thousands: u64) { 330 | self.print(prefix.bold()); 331 | if thousands == u64::MAX { 332 | self.println("Exceeds 2^64".dark_red().bold()); 333 | } else { 334 | let output = Logger::format_num(thousands); 335 | self.println(output.as_str().bold()); 336 | } 337 | } 338 | 339 | /// Format a number 340 | pub fn format_num(num: u64) -> String { 341 | let mut thousands = num as f64; 342 | let mut denomination = ""; 343 | let denominations = vec!["", "K", "M", "B", "T"]; 344 | for i in 0..denominations.len() { 345 | denomination = denominations[i]; 346 | if i == denominations.len() - 1 || thousands < 1000.0 { 347 | break; 348 | } 349 | thousands /= 1000.0; 350 | } 351 | if denomination.is_empty() || thousands >= 100.0 { 352 | format!("{:.0}{}", thousands, denomination) 353 | } else if thousands >= 10.0 { 354 | format!("{:.1}{}", thousands, denomination) 355 | } else { 356 | format!("{:.2}{}", thousands, denomination) 357 | } 358 | } 359 | 360 | /// Parse a formatted number 361 | pub fn parse_num(str: &str) -> Result { 362 | let denominations = vec!["", "K", "M", "B", "T"]; 363 | let mut thousands = 1_000_000_000_000_f64; 364 | for denom in denominations.iter().rev() { 365 | if str.contains(denom) { 366 | break; 367 | } 368 | thousands /= 1000.0; 369 | } 370 | let digits = str.chars().filter(|c| c.is_ascii_digit() || *c == '.'); 371 | match digits.collect::().parse::() { 372 | Ok(num) => Ok((num * thousands) as u64), 373 | Err(_) => bail!("Unable to parse num from '{}'", str), 374 | } 375 | } 376 | } 377 | 378 | #[cfg(test)] 379 | mod tests { 380 | use crate::logger::*; 381 | 382 | #[test] 383 | fn parses_numbers() { 384 | assert_eq!(Logger::parse_num(" 1.2M ").unwrap(), 1_200_000); 385 | assert_eq!(Logger::parse_num(" 2.34K/sec ").unwrap(), 2340); 386 | assert_eq!(Logger::parse_num(" 123 ").unwrap(), 123); 387 | } 388 | 389 | #[test] 390 | fn formats_tables() { 391 | let logger = Logger::new(); 392 | let table = logger.table(vec!["a---", "b", "c--"]); 393 | let formatted = table.format(vec!["1".to_string(), "2".to_string(), "3".to_string()]); 394 | assert_eq!(formatted, "1 |2|3 "); 395 | } 396 | 397 | #[tokio::test] 398 | async fn timer_starts_and_ends() { 399 | let timer = Logger::off().time("", 100).await; 400 | let handle = timer.start().await; 401 | timer.add(99); 402 | timer.add(1); 403 | handle.await.unwrap(); 404 | assert_eq!(timer.count(), 100); 405 | 406 | let timer = Logger::off().time_verbose("", 100, 10).await; 407 | let handle = timer.start().await; 408 | timer.add(50); 409 | timer.end(); 410 | handle.await.unwrap(); 411 | assert_eq!(timer.count(), 500); 412 | } 413 | 414 | #[test] 415 | fn formats_nums() { 416 | assert_eq!(Logger::format_num(123), "123"); 417 | assert_eq!(Logger::format_num(1230), "1.23K"); 418 | assert_eq!(Logger::format_num(12300), "12.3K"); 419 | assert_eq!(Logger::format_num(123000), "123K"); 420 | assert_eq!(Logger::format_num(56_700_000), "56.7M"); 421 | assert_eq!(Logger::format_num(56_700_000_000), "56.7B"); 422 | assert_eq!(Logger::format_num(56_700_000_000_000), "56.7T"); 423 | } 424 | 425 | #[test] 426 | fn formats_eta() { 427 | assert_eq!(Timer::format_eta(50.0, 60), "1 mins, 0 secs"); 428 | assert_eq!( 429 | Timer::format_eta(0.00001, 1), 430 | "115 days, 17 hours, 46 mins, 39 secs" 431 | ); 432 | } 433 | } 434 | -------------------------------------------------------------------------------- /src/benchmarks.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | use std::fs::File; 3 | use std::io; 4 | use std::io::Write; 5 | use std::path::PathBuf; 6 | 7 | use anyhow::{bail, Result}; 8 | use crossterm::style::Stylize; 9 | use tokio::task::JoinSet; 10 | use tokio::time::Instant; 11 | 12 | use crate::combination::Combinations; 13 | use crate::logger::{Attempt, Logger, Timer}; 14 | use crate::permutations::Permutations; 15 | use crate::seed::{Finished, Seed}; 16 | use crate::tests::{run_tests, Test}; 17 | use crate::{log_finished, BenchOption}; 18 | 19 | struct Benchmark { 20 | name: String, 21 | args: String, 22 | timer: Option, 23 | wall_time: u64, 24 | derivations: String, 25 | } 26 | 27 | impl Benchmark { 28 | fn new(name: &str, args: &str) -> Self { 29 | Self::with_derivations(name, "m/0/0", args) 30 | } 31 | 32 | fn with_derivations(name: &str, derivations: &str, args: &str) -> Self { 33 | Self { 34 | name: name.to_string(), 35 | args: args.to_string(), 36 | derivations: derivations.to_string(), 37 | timer: None, 38 | wall_time: 0, 39 | } 40 | } 41 | } 42 | 43 | /// Run all the benchmarks with the given options 44 | pub async fn run_benchmarks(mut option: BenchOption) -> Result<()> { 45 | let log = Logger::new(); 46 | 47 | if option.release { 48 | option.test = true; 49 | option.diff = Some("3090".to_string()); 50 | option.bench = true; 51 | option.pass = true 52 | } 53 | 54 | let mut benchmarks = vec![]; 55 | // dad moral begin apology cheap vast clerk limb shaft salt citizen awesome 56 | // aim twin nest escape combine lady grant ocean olympic post silent exist burger amateur physical muscle blossom series because dress cradle zone kick dove 57 | benchmarks.push(Benchmark::with_derivations("Master XPUB (mask attack)", "", "-s dad,moral,begin,apology,cheap,vast,clerk,limb,shaft,salt,citizen,awesome -p ?l?d?d?d?d?d?d -a xpub661MyMwAqRbcEZjJh7cPj6aGJ9NpRDUfpNz65bLKQQKR6dznUoszbxGyF7JUeCCNdYyboeD9EnRGgz8UfZW2hMzMBXA7SLumhtMU8VWy65L")); 58 | benchmarks.push(Benchmark::with_derivations("1000 derivations (mask attack)", "m/0/?9h/?9/?9", "-s dad,moral,begin,apology,cheap,vast,clerk,limb,shaft,salt,citizen,awesome -p ?l?d?d?d -a 18FkAx3zZNwmm6iTCcpHFxrrbs5sgKC6Wf")); 59 | benchmarks.push(Benchmark::with_derivations("10 derivations (mask attack)", "m/0/5h/5/?9", "-s dad,moral,begin,apology,cheap,vast,clerk,limb,shaft,salt,citizen,awesome -p ?l?d?d?d?d?d -a 18dAXjq3NG5uVBxe1cpcwrxfvJxeDWy9oQ")); 60 | benchmarks.push(Benchmark::new("1 derivations (mask attack)", "-s dad,moral,begin,apology,cheap,vast,clerk,limb,shaft,salt,citizen,awesome -p ?l?d?d?d?d?d?d -a 1A8nieZBBmXbwb4kvVpXBRdEpCaekiRhHH")); 61 | benchmarks.push(Benchmark::new("Missing first words of 12", "-s ?,?,begin,apology,cheap,va?,clerk,limb,shaft,salt,citizen,awesome -a 13PciouesvLmVAvmNxW4RhZyDkCGuqpwRY")); 62 | benchmarks.push(Benchmark::new("Missing first words of 24", "-s ?,?,nest,escape,combine,lady,grant,ocean,olympic,post,si?,exist,burger,amateur,physical,muscle,blossom,series,because,dress,cradle,zone,kick,dove -a 18qfTDrgRZa3ASKy6erJUCWLARaiFNyLty")); 63 | benchmarks.push(Benchmark::new("Permute 12 of 12 words", "-s dad,moral,begin,apology,cheap,vast,clerk,limb,shaft,salt,awesome,citizen -c 12 -a 13PciouesvLmVAvmNxW4RhZyDkCGuqpwRY")); 64 | benchmarks.push(Benchmark::new("Permute 12 of 24 words", "-s ^aim,^twin,^nest,^escape,^combine,^lady,^grant,^ocean,^olympic,^post,^silent,^exist,burger,amateur,physical,muscle,blossom,series,because,dress,cradle,zone,dove,kick -c 24 -a 18qfTDrgRZa3ASKy6erJUCWLARaiFNyLty")); 65 | benchmarks.push(Benchmark::new("Missing last words of 12", "-s dad,moral,begin,apology,cheap,va?,clerk,limb,shaft,salt,?,? -a 13PciouesvLmVAvmNxW4RhZyDkCGuqpwRY")); 66 | benchmarks.push(Benchmark::new("Missing last words of 24", "-s ai?,twin,nest,escape,combine,lady,grant,ocean,olympic,post,si?,exist,burger,amateur,physical,muscle,blossom,series,because,dress,cradle,zone,?,? -a 18qfTDrgRZa3ASKy6erJUCWLARaiFNyLty")); 67 | benchmarks.push(Benchmark::new("Passphrase dict+dict attack", "-s dad,moral,begin,apology,cheap,vast,clerk,limb,shaft,salt,citizen,awesome -p ./dicts/100k.txt,~ -p ./dicts/10k_upper.txt -a 17whoxEdasBPiEWKU1kjreNBaGBDzp2woS")); 68 | benchmarks.push(Benchmark::new("Passphrase dict+mask attack", "-s dad,moral,begin,apology,cheap,vast,clerk,limb,shaft,salt,citizen,awesome -p ./dicts/100k.txt -p ~?d?d?d -a 1Q6hStQLwApp4DERF57A5pJu9VsogRvCRA")); 69 | benchmarks.push(Benchmark::new("Passphrase mask+dict attack", "-s dad,moral,begin,apology,cheap,vast,clerk,limb,shaft,salt,citizen,awesome -p ?d?d?d -p ~,./dicts/100k.txt -a 1JVJrrWwaCS4FVREVNLULLGqZSqFC8dV9P")); 70 | benchmarks.push(Benchmark::new("Small passphrase + seed", "-s ?,moral,begin,apology,cheap,va?,clerk,limb,shaft,salt,citizen,awesome -p ?d?d -a 1DrJAfW6TY6X3q6SBmZHAUddfodzEuz6Mg")); 71 | benchmarks.push(Benchmark::new("Large passphrase + seed", "-s ?,moral,begin,apology,cheap,vast,clerk,limb,shaft,salt,citizen,awesome -p ?d?d?d?d?d -a 1FRm26FwcVtnRe2q8fHdd9c11UEEH5EYUo")); 72 | 73 | let file = match option.diff { 74 | None => None, 75 | Some(suffix) => { 76 | option.bench = true; 77 | let file = parse_benchmarks_file(suffix)?; 78 | for benchmark in &benchmarks { 79 | if !file.contains_key(&benchmark.name) { 80 | let err = format!("Missing '{}' from benchmark.txt", benchmark.name); 81 | log.println_err(&err); 82 | } 83 | } 84 | Some(file) 85 | } 86 | }; 87 | 88 | if option.test { 89 | run_tests().await?; 90 | } 91 | if !option.bench && !option.pass { 92 | return Ok(()); 93 | } 94 | 95 | let mut count = 0; 96 | let len = benchmarks.len(); 97 | for benchmark in &mut benchmarks { 98 | count += 1; 99 | let name = format!("benchmark '{}' {}/{}", benchmark.name, count, len); 100 | if option.pass { 101 | let out = format!("\n\n\n\n\nRunning passing {}", name); 102 | log.println(out.as_str().bold().dark_cyan()); 103 | let finished = run_benchmark(benchmark, &log, false, count).await; 104 | if finished.seed.is_none() { 105 | bail!("Benchmark '{}' did not pass", benchmark.name); 106 | } 107 | } 108 | 109 | if option.bench { 110 | let out = format!("\n\n\n\n\nRunning exhausting {}", name); 111 | log.println(out.as_str().bold().dark_cyan()); 112 | let finished = run_benchmark(benchmark, &log, true, count).await; 113 | if finished.seed.is_some() { 114 | bail!("Benchmark '{}' did not exhaust", benchmark.name); 115 | } 116 | } 117 | } 118 | 119 | log.println("\n\n\n\n\nBenchmark Results:".bold().dark_cyan()); 120 | let table = log.table(vec![ 121 | "Benchmark Name ", 122 | "Guesses ", 123 | "Speed ", 124 | "GPU Speed ", 125 | "Time ", 126 | "Wall Time", 127 | ]); 128 | table.log_heading(); 129 | for benchmark in &benchmarks { 130 | if let Some(timer) = &benchmark.timer { 131 | let guesses = Logger::format_num(timer.count()); 132 | let recovery_time = Timer::format_time(timer.seconds()); 133 | let wall_time = Timer::format_time(benchmark.wall_time); 134 | table.log_row(vec![ 135 | benchmark.name.clone(), 136 | guesses, 137 | timer.speed() + "/sec", 138 | timer.gpu_speed() + "/sec", 139 | recovery_time, 140 | wall_time, 141 | ]); 142 | } 143 | } 144 | 145 | if let Some(file) = file { 146 | log.println( 147 | "\n\n\n\n\nBenchmark Differences (>100% is improvement):" 148 | .bold() 149 | .dark_cyan(), 150 | ); 151 | let table = log.table(vec![ 152 | "Benchmark Name ", 153 | "Guesses ", 154 | "Speed ", 155 | ]); 156 | table.log_heading(); 157 | for benchmark in benchmarks { 158 | let file_metrics = file.get(&benchmark.name); 159 | match (file_metrics, benchmark.timer) { 160 | (Some(metrics1), Some(metrics2)) => { 161 | let guess = (metrics2.count() as f64) / metrics1.guesses * 100.0; 162 | let speed = 163 | (Logger::parse_num(&metrics2.speed())? as f64) / metrics1.speed * 100.0; 164 | table.log_row(vec![ 165 | benchmark.name.clone(), 166 | format!("{}%", guess as u64), 167 | format!("{}%", speed as u64), 168 | ]); 169 | } 170 | _ => table.log_row(vec![ 171 | benchmark.name, 172 | "Not Found".to_string(), 173 | "Not Found".to_string(), 174 | ]), 175 | } 176 | } 177 | } 178 | Ok(()) 179 | } 180 | 181 | struct BenchmarkFile { 182 | guesses: f64, 183 | speed: f64, 184 | } 185 | 186 | fn parse_benchmarks_file(suffix: String) -> Result> { 187 | let name = format!("benchmarks_{}.txt", suffix); 188 | let path = PathBuf::from("docs").join(&name); 189 | let text = match File::open(path).and_then(io::read_to_string) { 190 | Ok(text) => text, 191 | Err(_) => bail!("Unable to read '{}'", name), 192 | }; 193 | let mut map = BTreeMap::new(); 194 | for line in text.lines().skip(1) { 195 | let mut split = line.split("|"); 196 | let name = split.next().expect("has column 1"); 197 | let guesses = split.next().expect("has column 2"); 198 | let speed = split.next().expect("has column 3"); 199 | let benchmark = BenchmarkFile { 200 | guesses: Logger::parse_num(guesses)? as f64, 201 | speed: Logger::parse_num(speed)? as f64, 202 | }; 203 | let remove_trailing = name.split_whitespace().collect::>().join(" "); 204 | map.insert(remove_trailing, benchmark); 205 | } 206 | Ok(map) 207 | } 208 | 209 | async fn run_benchmark( 210 | benchmark: &mut Benchmark, 211 | log: &Logger, 212 | exhaust: bool, 213 | id: usize, 214 | ) -> Finished { 215 | let mut derivation = benchmark.derivations.clone(); 216 | let mut args = benchmark.args.clone(); 217 | if exhaust { 218 | derivation = derivation.replace("m/0", "m/1"); 219 | args = args.replace("awesome", "flower"); 220 | args = args.replace("?d ", "?d?d "); 221 | args = args.replace("va?", "?"); 222 | args = args.replace("si?", "?"); 223 | args = args.replace("ai?", "?"); 224 | args = args.replace("^exist", "exist"); 225 | } 226 | let name = if exhaust { 227 | format!("hc_bench_exhaust{}", id) 228 | } else { 229 | format!("hc_bench_pass{}", id) 230 | }; 231 | if !derivation.is_empty() { 232 | args = format!("-d {} {}", derivation, args); 233 | } 234 | let mut hashcat = Test::configure(&name, &args, &log); 235 | 236 | let now = Instant::now(); 237 | let (timer, finished) = hashcat.run(&log, exhaust).await.unwrap(); 238 | benchmark.timer = Some(timer); 239 | benchmark.wall_time = now.elapsed().as_secs(); 240 | log_finished(&finished, &log); 241 | finished 242 | } 243 | 244 | #[allow(dead_code)] 245 | pub async fn benchmark_permutations() { 246 | let vec = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]; 247 | let choose = 10; 248 | let mut perm = Permutations::new(vec.clone(), choose); 249 | 250 | let mut set = JoinSet::new(); 251 | let time = Instant::now(); 252 | for mut p in perm.shard(100) { 253 | set.spawn(async move { 254 | let mut count = 0; 255 | while let Some(_) = p.next() { 256 | count += 1; 257 | } 258 | count 259 | }); 260 | } 261 | let mut count = 0; 262 | while let Some(c) = set.join_next().await { 263 | count += c.unwrap(); 264 | } 265 | println!("ITERATIONS: {}", count); 266 | println!("ELAPSED: {:?}", time.elapsed().as_millis()); 267 | 268 | let mut count = 0; 269 | while let Some(_) = perm.next() { 270 | count += 1; 271 | } 272 | println!("ITERATIONS: {}", count); 273 | println!("ELAPSED: {:?}", time.elapsed().as_millis()); 274 | } 275 | 276 | #[allow(dead_code)] 277 | pub async fn benchmark_combinations1() { 278 | let path = PathBuf::from("dicts"); 279 | let file1 = io::read_to_string(File::open(path.join("10k.txt")).unwrap()).unwrap(); 280 | let file2 = io::read_to_string(File::open(path.join("100k.txt")).unwrap()).unwrap(); 281 | let lines1: Vec<_> = file1.lines().map(|str| str.to_string()).collect(); 282 | let lines2: Vec<_> = file2.lines().map(|str| str.to_string()).collect(); 283 | let mut combinations = Combinations::new(vec![lines1, lines2]); 284 | while let Some(_) = combinations.next() {} 285 | // let log = Logger::new(); 286 | // combinations.write_zip("/tmp/test.gz", &log).await.unwrap(); 287 | } 288 | 289 | // ~1B permutations in ~3635ms 290 | #[allow(dead_code)] 291 | pub async fn benchmark_combinations2() { 292 | let mut list = vec![]; 293 | let mut index = vec![]; 294 | for i in 0..13 { 295 | list.push(vec![0]); 296 | index.push(i); 297 | } 298 | let mut combinations = Combinations::permute(list, index, 10); 299 | println!("Permutations: {}", combinations.permutations()); 300 | println!("Estimated: {}", combinations.total()); 301 | println!("Exact : {}", combinations.estimate_total(u64::MAX)); 302 | 303 | let mut set = JoinSet::new(); 304 | let time = Instant::now(); 305 | for mut p in combinations.shard(100) { 306 | set.spawn(async move { 307 | let mut count = 0; 308 | while let Some(_) = p.next() { 309 | count += 1; 310 | } 311 | count 312 | }); 313 | } 314 | let mut count = 0; 315 | while let Some(c) = set.join_next().await { 316 | count += c.unwrap(); 317 | } 318 | println!("ITERATIONS: {}", count); 319 | println!("ELAPSED: {:?}", time.elapsed().as_millis()); 320 | 321 | let time = Instant::now(); 322 | let mut count = 0; 323 | while let Some(_) = combinations.next() { 324 | count += 1; 325 | } 326 | println!("ITERATIONS: {}", count); 327 | println!("ELAPSED: {:?}", time.elapsed().as_millis()); 328 | } 329 | 330 | // 800M 331 | #[allow(dead_code)] 332 | pub async fn benchmark_seed() { 333 | let seed = Seed::from_args( 334 | "music,eternal,upper,myth,slight,divide,voyage,afford,q?,e?,e?,e?,e?,abandon,zoo", 335 | &None, 336 | ) 337 | .unwrap(); 338 | println!("Total: {}", seed.total()); 339 | 340 | let mut set = JoinSet::new(); 341 | let time = Instant::now(); 342 | for mut s in seed.shard_words(100) { 343 | set.spawn(async move { 344 | let mut count = 0; 345 | while let Some(_) = s.next_valid() { 346 | count += 1; 347 | } 348 | count 349 | }); 350 | } 351 | let mut count = 0; 352 | while let Some(c) = set.join_next().await { 353 | count += c.unwrap(); 354 | } 355 | println!("ITERATIONS: {}", count); 356 | println!("ELAPSED: {:?}", time.elapsed().as_millis()); 357 | } 358 | 359 | // 1B combinations in ~450ms 360 | #[allow(dead_code)] 361 | pub async fn benchmark_combinations3() { 362 | let mut list = vec![]; 363 | for _ in 0..9 { 364 | list.push(vec![0; 10]); 365 | } 366 | let mut combinations = Combinations::permute(list, vec![], 9); 367 | println!("Permutations: {}", combinations.permutations()); 368 | println!("Estimated: {}", combinations.total()); 369 | println!("Exact : {}", combinations.estimate_total(u64::MAX)); 370 | 371 | let mut set = JoinSet::new(); 372 | let time = Instant::now(); 373 | for mut p in combinations.shard(100) { 374 | set.spawn(async move { 375 | let mut count = 0; 376 | while let Some(_) = p.next() { 377 | count += 1; 378 | } 379 | count 380 | }); 381 | } 382 | let mut count = 0; 383 | while let Some(c) = set.join_next().await { 384 | count += c.unwrap(); 385 | } 386 | println!("ITERATIONS: {}", count); 387 | println!("ELAPSED: {:?}", time.elapsed().as_millis()); 388 | 389 | let time = Instant::now(); 390 | let mut count = 0; 391 | while let Some(_) = combinations.next() { 392 | count += 1; 393 | } 394 | println!("ITERATIONS: {}", count); 395 | println!("ELAPSED: {:?}", time.elapsed().as_millis()); 396 | } 397 | 398 | // Generate dicts of popular words from https://norvig.com/ngrams/ 399 | #[allow(dead_code)] 400 | pub fn dicts() { 401 | let root = PathBuf::from("dicts"); 402 | for (count, name) in vec![10, 1000, 10_000, 100_000] 403 | .iter() 404 | .zip(vec!["test", "1k", "10k", "100k"]) 405 | { 406 | for kind in vec!["", "_upper", "_cap"] { 407 | let path = root.join("norvig.com_ngrams_count_1w.txt"); 408 | let raw = File::open(path).expect("File exists"); 409 | let filename = format!("{}{}.txt", name, kind); 410 | let mut file = File::create(root.join(filename)).unwrap(); 411 | let mut written = 0; 412 | 413 | for line in io::read_to_string(raw).unwrap().lines() { 414 | if written == *count { 415 | continue; 416 | } 417 | 418 | let word: &str = line.split("\t").next().unwrap(); 419 | if kind == "_upper" { 420 | writeln!(file, "{}", word.to_uppercase()).unwrap(); 421 | } else if kind == "_cap" { 422 | let mut c = word.chars(); 423 | let upper: String = c.next().unwrap().to_uppercase().chain(c).collect(); 424 | writeln!(file, "{}", upper).unwrap(); 425 | } else { 426 | writeln!(file, "{}", word).unwrap(); 427 | } 428 | 429 | written += 1; 430 | } 431 | } 432 | } 433 | } 434 | -------------------------------------------------------------------------------- /src/combination.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::min; 2 | use std::collections::BTreeSet; 3 | use std::fmt::Debug; 4 | use std::fs::File; 5 | use std::io::{BufWriter, Write}; 6 | 7 | use anyhow::{format_err, Error, Result}; 8 | use gzp::deflate::Gzip; 9 | use gzp::par::compress::{ParCompress, ParCompressBuilder}; 10 | use gzp::ZWriter; 11 | 12 | use crate::logger::Logger; 13 | use crate::permutations::Permutations; 14 | 15 | /// Generates combinations of elements in a fast way 16 | #[derive(Debug, Clone)] 17 | pub struct Combinations { 18 | permute_indices: BTreeSet, 19 | elements: Vec>, 20 | indices: Vec, 21 | next: Vec, 22 | position: u64, 23 | combinations: u64, 24 | permutations: Permutations, 25 | length: usize, 26 | permutation: Vec, 27 | } 28 | 29 | impl Combinations { 30 | pub fn new(elements: Vec>) -> Self { 31 | let len = elements.len(); 32 | Self::permute(elements, vec![], len) 33 | } 34 | 35 | /// Generates all combinations of elements of a given length, permuting the given indices 36 | pub fn permute(elements: Vec>, permute_indices: Vec, length: usize) -> Self { 37 | let permute_len = permute_indices.len() - (elements.len() - length); 38 | let mut permutations = Permutations::new(permute_indices.clone(), permute_len); 39 | let permute_indices = BTreeSet::from_iter(permute_indices.into_iter()); 40 | let permutation = permutations.next().unwrap_or(&vec![]).clone(); 41 | 42 | Self::new_shard(elements, permutations, permute_indices, length, permutation) 43 | } 44 | 45 | fn new_shard( 46 | elements: Vec>, 47 | permutations: Permutations, 48 | permute_indices: BTreeSet, 49 | length: usize, 50 | permutation: Vec, 51 | ) -> Self { 52 | let indices = vec![0; elements.len()]; 53 | Self { 54 | permute_indices, 55 | permutations, 56 | permutation, 57 | elements, 58 | indices, 59 | next: vec![], 60 | position: 0, 61 | combinations: 1, 62 | length, 63 | } 64 | } 65 | 66 | /// Returns Some(element) if the position is fixed, otherwise None 67 | pub fn fixed_positions(&self) -> Vec> { 68 | let mut fixed = vec![]; 69 | for i in 0..self.len() { 70 | if !self.permute_indices.contains(&i) && self.elements[i].len() == 1 { 71 | fixed.push(Some(self.elements[i][0].clone())); 72 | } else { 73 | fixed.push(None); 74 | } 75 | } 76 | fixed 77 | } 78 | 79 | /// Returns the first combination we will produce in O(1) 80 | pub fn begin(&self) -> Vec { 81 | let mut vec = vec![]; 82 | for i in 0..self.length { 83 | vec.push(self.elements[i][0].clone()); 84 | } 85 | vec 86 | } 87 | 88 | /// Returns the last combination we will produce in O(1) 89 | pub fn end(&self) -> Vec { 90 | let mut vec = vec![]; 91 | let mut permute = self.permute_indices.clone(); 92 | for i in 0..self.length { 93 | let mut j = i; 94 | if self.permute_indices.contains(&i) { 95 | j = permute.pop_last().unwrap(); 96 | } 97 | let len = self.elements[j].len(); 98 | vec.push(self.elements[j][len - 1].clone()); 99 | } 100 | vec 101 | } 102 | 103 | /// Return a copy of all elements 104 | pub fn elements(&self) -> Vec> { 105 | self.elements.clone() 106 | } 107 | 108 | /// Returns the total combinations, estimating for >10M which is generally fast and accurate 109 | pub fn total(&self) -> u64 { 110 | self.estimate_total(10_000_000) 111 | } 112 | 113 | /// Returns an estimate of the total for a given sample size 114 | pub fn estimate_total(&self, sample_size: u64) -> u64 { 115 | let mut total_combo = 1_u64; 116 | let mut total_perm = 0_u64; 117 | let mut sizes = vec![]; 118 | 119 | for i in 0..self.elements.len() { 120 | let len = self.elements[i].len() as u64; 121 | if self.permute_indices.contains(&i) { 122 | sizes.push(self.elements[i].len() as u64); 123 | } else { 124 | total_combo = total_combo.saturating_mul(len); 125 | } 126 | } 127 | if sizes.len() == 0 { 128 | return total_combo; 129 | } 130 | 131 | let mut count = 0; 132 | let mut permutations = Permutations::new(sizes, self.permutation.len()); 133 | let num_permutations = self.permutations() as f64; 134 | while let Some(next) = permutations.next() { 135 | count += 1; 136 | total_perm = total_perm.saturating_add(next.iter().product()); 137 | if count == sample_size { 138 | total_perm = (total_perm as f64 * (num_permutations / sample_size as f64)) as u64; 139 | break; 140 | } 141 | } 142 | 143 | total_perm.saturating_mul(total_combo) 144 | } 145 | 146 | /// Returns the number of permutations (without considering combinations) 147 | pub fn permutations(&self) -> u64 { 148 | let n = self.permute_indices.len() as u64; 149 | let r = self.permutation.len() as u64; 150 | let mut permutations = 1_u64; 151 | for i in n - r + 1..=n { 152 | permutations = permutations.saturating_mul(i); 153 | } 154 | permutations 155 | } 156 | 157 | /// Returns the length of the output 158 | pub fn len(&self) -> usize { 159 | self.length 160 | } 161 | 162 | fn next_index_rev(&self, index: &usize, permutation_index: &mut usize) -> usize { 163 | if self.permute_indices.contains(&index) { 164 | *permutation_index -= 1; 165 | return self.permutation[*permutation_index]; 166 | } 167 | return *index; 168 | } 169 | 170 | fn combinations(&self) -> u64 { 171 | let mut permutation_index = self.permutation.len(); 172 | let mut combinations = 1_u64; 173 | for i in (0..self.length).rev() { 174 | let j = self.next_index_rev(&i, &mut permutation_index); 175 | combinations = combinations.saturating_mul(self.elements[j].len() as u64); 176 | } 177 | combinations 178 | } 179 | 180 | fn next_permute(&mut self) { 181 | if self.position == self.combinations && self.permutations.len() > 1 { 182 | if let Some(permutation) = self.permutations.next() { 183 | self.permutation = permutation.clone(); 184 | self.combinations = self.combinations(); 185 | self.position = 0; 186 | self.indices = vec![0; self.elements.len()]; 187 | } 188 | } 189 | } 190 | 191 | /// Returns the next combination, or None if we are finished 192 | pub fn next(&mut self) -> Option<&Vec> { 193 | if self.position >= self.combinations { 194 | return None; 195 | } 196 | 197 | self.position += 1; 198 | let mut permutation_index = self.permutation.len(); 199 | 200 | if self.position == 1 { 201 | self.next.clear(); 202 | for i in (0..self.length).rev() { 203 | let j = self.next_index_rev(&i, &mut permutation_index); 204 | self.next.push(self.elements[j][0].clone()); 205 | } 206 | self.combinations = self.combinations(); 207 | self.next.reverse(); 208 | self.next_permute(); 209 | return Some(&self.next); 210 | } 211 | 212 | for i in (0..self.length).rev() { 213 | let j = self.next_index_rev(&i, &mut permutation_index); 214 | 215 | if self.indices[j] < self.elements[j].len() - 1 { 216 | self.indices[j] += 1; 217 | self.next[i] = self.elements[j][self.indices[j]].clone(); 218 | break; 219 | } else { 220 | self.indices[j] = 0; 221 | self.next[i] = self.elements[j][0].clone(); 222 | } 223 | } 224 | self.next_permute(); 225 | Some(&self.next) 226 | } 227 | 228 | // Splits combinations into a minimum number of shards 229 | pub fn shard(&self, num: usize) -> Vec> { 230 | let mut shards = vec![]; 231 | 232 | if self.permutations.len() > 1 { 233 | let perm_shards = min(num as u64, self.permutations.len()) as usize; 234 | for mut perm in self.permutations.shard(perm_shards) { 235 | let permutation = perm.next().unwrap_or(&vec![]).clone(); 236 | shards.push(Self::new_shard( 237 | self.elements.clone(), 238 | perm, 239 | self.permute_indices.clone(), 240 | self.length, 241 | permutation, 242 | )); 243 | } 244 | } else { 245 | shards.push(self.clone()); 246 | } 247 | 248 | for i in 0..self.elements.len() { 249 | if !self.permute_indices.contains(&i) { 250 | shards = Self::shard_index(shards, i); 251 | if shards.len() >= num { 252 | break; 253 | } 254 | } 255 | } 256 | 257 | shards 258 | } 259 | 260 | fn shard_index(shards: Vec>, index: usize) -> Vec> { 261 | let mut next_shards = vec![]; 262 | for s in shards { 263 | for choice in &s.elements[index] { 264 | let mut elements = s.elements.clone(); 265 | elements[index] = vec![choice.clone()]; 266 | let ns = Self::new_shard( 267 | elements, 268 | s.permutations.clone(), 269 | s.permute_indices.clone(), 270 | s.length, 271 | s.permutation.clone(), 272 | ); 273 | next_shards.push(ns); 274 | } 275 | } 276 | next_shards 277 | } 278 | } 279 | 280 | impl Combinations { 281 | /// Write all combinations to a gz in parallel (very fast with multiple CPUs) 282 | pub async fn write_zip(&mut self, filename: &str, log: &Logger) -> Result<()> { 283 | let err = format_err!("Failed to create gzip file '{:?}'", filename); 284 | let file = File::create(filename).map_err(|_| err)?; 285 | let writer = BufWriter::new(file); 286 | let logname = format!("Writing Dictionary '{}'", filename); 287 | let timer = log.time(&logname, self.total()).await; 288 | let timer_handle = timer.start().await; 289 | 290 | let mut parz: ParCompress = ParCompressBuilder::new().from_writer(writer); 291 | let mut as_bytes = self.to_bytes(); 292 | while let Some(strs) = as_bytes.next() { 293 | for str in strs { 294 | parz.write_all(str).expect("Failed to write"); 295 | } 296 | parz.write(&[10]).unwrap(); 297 | timer.add(1); 298 | } 299 | 300 | parz.finish().map_err(Error::msg)?; 301 | timer_handle.await.expect("Timer failed"); 302 | Ok(()) 303 | } 304 | 305 | fn to_bytes(&self) -> Combinations<&[u8]> { 306 | let mut vecs = vec![]; 307 | for element in &self.elements { 308 | let mut vec = vec![]; 309 | for str in element { 310 | vec.push(str.as_bytes()); 311 | } 312 | vecs.push(vec); 313 | } 314 | Combinations::new(vecs) 315 | } 316 | } 317 | 318 | #[cfg(test)] 319 | mod tests { 320 | use crate::combination::*; 321 | 322 | fn expand(seeds: Vec>) -> Vec> { 323 | let mut expanded = vec![]; 324 | let mut set = BTreeSet::new(); 325 | for mut seed in seeds { 326 | while let Some(next) = seed.next() { 327 | assert_eq!(set.contains(next), false); 328 | set.insert(next.clone()); 329 | expanded.push(next.clone()); 330 | } 331 | } 332 | expanded 333 | } 334 | 335 | #[test] 336 | fn can_get_begin_and_end() { 337 | let combinations = Combinations::permute( 338 | vec![vec![1], vec![2], vec![3], vec![4]], 339 | vec![0, 1, 2, 3], 340 | 3, 341 | ); 342 | assert_eq!(combinations.begin(), vec![1, 2, 3]); 343 | assert_eq!(combinations.end(), vec![4, 3, 2]); 344 | } 345 | 346 | #[test] 347 | fn can_shard() { 348 | let combinations = Combinations::new(vec![vec![1, 2], vec![3, 4], vec![5, 6], vec![7, 8]]); 349 | assert_eq!( 350 | expand(vec![combinations.clone()]), 351 | expand(combinations.shard(3)) 352 | ); 353 | 354 | let combinations = Combinations::new(vec![vec![1, 2], vec![3]]); 355 | assert_eq!( 356 | expand(vec![combinations.clone()]), 357 | expand(combinations.shard(100)) 358 | ); 359 | 360 | let combinations = Combinations::permute( 361 | vec![vec![1, 2], vec![3, 4], vec![5, 6], vec![7, 8]], 362 | vec![0, 1, 2, 3], 363 | 2, 364 | ); 365 | let shards = combinations.shard(1000); 366 | assert_eq!(shards.len(), 12); 367 | assert_eq!( 368 | expand(vec![combinations.clone()]).len(), 369 | expand(shards).len() 370 | ); 371 | 372 | let combinations = Combinations::permute( 373 | vec![vec![1, 2, 3], vec![4, 5], vec![6], vec![7]], 374 | vec![1, 2, 3], 375 | 2, 376 | ); 377 | let shards = combinations.shard(100); 378 | assert_eq!(shards.len(), 9); 379 | assert_eq!(expand(vec![combinations.clone()]), expand(shards)); 380 | } 381 | 382 | #[test] 383 | fn writes_permutations1() { 384 | let mut combinations = 385 | Combinations::permute(vec![vec![1, 2], vec![3], vec![4]], vec![0, 1, 2], 2); 386 | assert_eq!(combinations.next(), Some(&vec![1, 3])); 387 | assert_eq!(combinations.next(), Some(&vec![2, 3])); 388 | assert_eq!(combinations.next(), Some(&vec![3, 1])); 389 | assert_eq!(combinations.next(), Some(&vec![3, 2])); 390 | assert_eq!(combinations.next(), Some(&vec![1, 4])); 391 | assert_eq!(combinations.next(), Some(&vec![2, 4])); 392 | assert_eq!(combinations.next(), Some(&vec![4, 1])); 393 | assert_eq!(combinations.next(), Some(&vec![4, 2])); 394 | assert_eq!(combinations.next(), Some(&vec![3, 4])); 395 | assert_eq!(combinations.next(), Some(&vec![4, 3])); 396 | assert_eq!(combinations.next(), None); 397 | 398 | let combinations = 399 | Combinations::permute(vec![vec![1, 2], vec![4, 5], vec![7, 8]], vec![0, 1, 2], 3); 400 | permute_assert(combinations, 6, 48); 401 | 402 | let combinations = Combinations::permute( 403 | vec![vec![1, 2, 3], vec![4, 5, 6], vec![7, 8, 9]], 404 | vec![0, 1, 2], 405 | 2, 406 | ); 407 | permute_assert(combinations, 6, 54); 408 | 409 | let combinations = Combinations::permute( 410 | vec![vec![10, 11], vec![1, 2, 3], vec![4, 5, 6], vec![7, 8, 9]], 411 | vec![1, 2, 3], 412 | 3, 413 | ); 414 | permute_assert(combinations, 6, 108); 415 | 416 | let combinations = Combinations::permute( 417 | vec![ 418 | vec![0, 1, 2], 419 | vec![3, 4], 420 | vec![5, 6, 7, 8, 9], 421 | vec![10, 11, 12], 422 | ], 423 | vec![0, 1, 2, 3], 424 | 3, 425 | ); 426 | permute_assert(combinations, 24, 738); 427 | } 428 | 429 | fn permute_assert(mut combinations: Combinations, permutations: u64, exact: u64) { 430 | assert_eq!(combinations.permutations(), permutations); 431 | assert_eq!(combinations.total(), exact); 432 | 433 | let mut perms = vec![]; 434 | while let Some(next) = combinations.next() { 435 | assert!(!perms.contains(next)); 436 | perms.push(next.clone()); 437 | } 438 | assert_eq!(perms.len() as u64, exact); 439 | } 440 | 441 | #[test] 442 | fn writes_permutations2() { 443 | let mut combinations = 444 | Combinations::permute(vec![vec![1], vec![2], vec![3, 4]], vec![0, 2], 3); 445 | assert_eq!(combinations.total(), 4); 446 | assert_eq!(combinations.next(), Some(&vec![1, 2, 3])); 447 | assert_eq!(combinations.next(), Some(&vec![1, 2, 4])); 448 | assert_eq!(combinations.next(), Some(&vec![3, 2, 1])); 449 | assert_eq!(combinations.next(), Some(&vec![4, 2, 1])); 450 | assert_eq!(combinations.next(), None); 451 | } 452 | 453 | #[test] 454 | fn writes_all_combinations1() { 455 | let mut combinations = Combinations::new(vec![vec![1, 2], vec![3, 4], vec![5, 6, 7]]); 456 | assert_eq!(combinations.next(), Some(&vec![1, 3, 5])); 457 | assert_eq!(combinations.next(), Some(&vec![1, 3, 6])); 458 | assert_eq!(combinations.next(), Some(&vec![1, 3, 7])); 459 | assert_eq!(combinations.next(), Some(&vec![1, 4, 5])); 460 | assert_eq!(combinations.next(), Some(&vec![1, 4, 6])); 461 | assert_eq!(combinations.next(), Some(&vec![1, 4, 7])); 462 | assert_eq!(combinations.next(), Some(&vec![2, 3, 5])); 463 | assert_eq!(combinations.next(), Some(&vec![2, 3, 6])); 464 | assert_eq!(combinations.next(), Some(&vec![2, 3, 7])); 465 | assert_eq!(combinations.next(), Some(&vec![2, 4, 5])); 466 | assert_eq!(combinations.next(), Some(&vec![2, 4, 6])); 467 | assert_eq!(combinations.next(), Some(&vec![2, 4, 7])); 468 | assert_eq!(combinations.next(), None); 469 | } 470 | 471 | #[test] 472 | fn writes_all_combinations2() { 473 | let mut combinations = Combinations::new(vec![vec![1, 2], vec![3], vec![4, 5]]); 474 | assert_eq!(combinations.next(), Some(&vec![1, 3, 4])); 475 | assert_eq!(combinations.next(), Some(&vec![1, 3, 5])); 476 | assert_eq!(combinations.next(), Some(&vec![2, 3, 4])); 477 | assert_eq!(combinations.next(), Some(&vec![2, 3, 5])); 478 | assert_eq!(combinations.next(), None); 479 | } 480 | } 481 | -------------------------------------------------------------------------------- /src/passphrase.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | use std::fs::File; 3 | use std::io; 4 | use std::path::PathBuf; 5 | 6 | use anyhow::{bail, format_err, Error, Result}; 7 | 8 | use crate::combination::Combinations; 9 | use crate::logger::{Attempt, Logger}; 10 | use crate::{HASHCAT_PATH, SEPARATOR}; 11 | 12 | const ERR_MSG: &str = "\nPassphrase takes at most 2 args with the following possibilities: 13 | DICT attack: --passphrase 'prefix,./dicts/dict.txt,suffix' 14 | MASK attack: --passphrase 'prefix?l?l?l?d?d?1suffix' 15 | DICT DICT attack: --passphrase './dicts/dict.txt,deliminator' './dicts/dict.txt' 16 | DICT DICT DICT attack: --passphrase './dict1.txt,deliminator1,./dict2.txt,deliminator2' './dict3.txt' 17 | DICT MASK attack: --passphrase './dict.txt' '?l?l?l?d?1' 18 | MASK DICT attack: --passphrase '?l?l?l?d?1' './dict.txt' 19 | 20 | DICT files should be comma-separated relative paths starting with './' or deliminators 21 | MASK attacks should contain a mix of wildcards and normal characters 22 | To escape special characters '?' ',' '/' just double them, e.g. '??' ',,' '//'\n"; 23 | 24 | const MAX_DICT: u64 = 1_000_000_000; 25 | const HC_LEFT_DICT: &str = "_left.gz"; 26 | const HC_RIGHT_DICT: &str = "_right.gz"; 27 | 28 | #[derive(Debug, Clone)] 29 | pub struct Passphrase { 30 | pub attack_mode: usize, 31 | left: PassphraseArg, 32 | right: Option, 33 | charsets: UserCharsets, 34 | } 35 | 36 | impl Attempt for Passphrase { 37 | fn total(&self) -> u64 { 38 | let mut total = 1; 39 | total *= Self::attempt(&self.left).total(); 40 | if let Some(right) = &self.right { 41 | total *= Self::attempt(&right).total(); 42 | } 43 | total 44 | } 45 | 46 | fn begin(&self) -> String { 47 | if let Some(right) = &self.right { 48 | return Self::attempt(&self.left).begin() + &Self::attempt(&right).begin(); 49 | } 50 | Self::attempt(&self.left).begin() 51 | } 52 | 53 | fn end(&self) -> String { 54 | if let Some(right) = &self.right { 55 | return Self::attempt(&self.left).end() + &Self::attempt(&right).end(); 56 | } 57 | Self::attempt(&self.left).end() 58 | } 59 | } 60 | 61 | impl Passphrase { 62 | pub fn empty_mask() -> Self { 63 | Self::new( 64 | 3, 65 | vec![PassphraseArg::Mask(Mask::empty())], 66 | UserCharsets::empty(), 67 | ) 68 | } 69 | 70 | fn new(attack_mode: usize, args: Vec, charsets: UserCharsets) -> Self { 71 | let mut args = args.into_iter(); 72 | Self { 73 | attack_mode, 74 | left: args.next().expect("at least one arg"), 75 | right: args.next(), 76 | charsets, 77 | } 78 | } 79 | 80 | fn attempt(arg: &PassphraseArg) -> Box { 81 | match arg { 82 | PassphraseArg::Dict(d) => Box::new(d.clone()), 83 | PassphraseArg::Mask(m) => Box::new(m.clone()), 84 | } 85 | } 86 | 87 | pub async fn build_args(&self, prefix: &str, log: &Logger) -> Result> { 88 | let mut result = vec![]; 89 | result.push("-a".to_string()); 90 | result.push(self.attack_mode.to_string()); 91 | let dict = prefix.to_string() + HC_LEFT_DICT; 92 | result.push(Self::build_arg(&self.left, dict, log).await?); 93 | 94 | if let Some(right) = &self.right { 95 | let dict = prefix.to_string() + HC_RIGHT_DICT; 96 | result.push(Self::build_arg(right, dict, log).await?); 97 | } 98 | 99 | for charset in self.charsets.to_wildcards() { 100 | result.push(format!("-{}", charset.flag)); 101 | result.push(charset.charset.unwrap().to_string()); 102 | } 103 | 104 | Ok(result) 105 | } 106 | 107 | pub fn add_binary_charsets(&self, guesses: usize, entropy_bits: usize) -> Result> { 108 | let mut copy = self.clone(); 109 | 110 | let wildcards = copy.charsets.add_binary_charsets(entropy_bits)?; 111 | // Unable to generate the 3 wildcards required 112 | if wildcards.len() != 3 { 113 | return Ok(None); 114 | } 115 | if let PassphraseArg::Dict(d) = &self.left { 116 | if copy.right.is_none() { 117 | copy.right = Some(PassphraseArg::Dict(d.clone())); 118 | copy.left = PassphraseArg::Mask(Mask::empty()); 119 | copy.attack_mode = 7; 120 | } 121 | } 122 | 123 | if let PassphraseArg::Mask(ref mut m) = &mut copy.left { 124 | m.prefix_wild(&wildcards[2]); 125 | for _ in 1..guesses { 126 | m.prefix_wild(&wildcards[1]); 127 | m.prefix_wild(&wildcards[0]); 128 | } 129 | 130 | return Ok(Some(copy)); 131 | } 132 | Ok(None) 133 | } 134 | 135 | async fn build_arg(arg: &PassphraseArg, dictname: String, log: &Logger) -> Result { 136 | Ok(match arg { 137 | PassphraseArg::Mask(m) => m.arg.clone(), 138 | PassphraseArg::Dict(d) => { 139 | let mut dict = d.clone(); 140 | dict.combinations.write_zip(&dictname, log).await?; 141 | dictname 142 | } 143 | }) 144 | } 145 | 146 | pub fn from_arg(args: &Vec, charsets: &Vec>) -> Result { 147 | let charsets = UserCharsets::new(charsets.clone())?; 148 | let mut parsed = vec![]; 149 | for arg in args { 150 | parsed.push(Self::validate_arg(arg, &charsets)?); 151 | } 152 | 153 | let passphrase = match parsed[..] { 154 | [PassphraseArg::Mask(_)] => Passphrase::new(3, parsed, charsets), 155 | [PassphraseArg::Dict(_)] => Passphrase::new(0, parsed, charsets), 156 | [PassphraseArg::Dict(_), PassphraseArg::Dict(_)] => { 157 | Passphrase::new(1, parsed, charsets) 158 | } 159 | [PassphraseArg::Dict(_), PassphraseArg::Mask(_)] => { 160 | Passphrase::new(6, parsed, charsets) 161 | } 162 | [PassphraseArg::Mask(_), PassphraseArg::Dict(_)] => { 163 | Passphrase::new(7, parsed, charsets) 164 | } 165 | _ => bail!("Invalid passphrase args {:?}{}", args, ERR_MSG), 166 | }; 167 | 168 | Ok(passphrase) 169 | } 170 | 171 | fn validate_arg(arg: &str, charsets: &UserCharsets) -> Result { 172 | if arg.replace("??", "").contains("?") { 173 | Ok(PassphraseArg::Mask(Self::mask(arg, &charsets)?)) 174 | } else { 175 | Ok(PassphraseArg::Dict(Self::dict(arg)?)) 176 | } 177 | } 178 | 179 | fn dict(arg: &str) -> Result { 180 | let mut combinations: Vec> = vec![]; 181 | for arg in arg.split(SEPARATOR) { 182 | if arg.starts_with("./") && !arg.starts_with(".//") { 183 | let path = PathBuf::from_iter(arg.split("/").into_iter()); 184 | let err = format_err!("Failed to read file '{:?}'{}", path, ERR_MSG); 185 | let file = File::open(path).map_err(|_| err)?; 186 | let str = io::read_to_string(file).map_err(Error::msg)?; 187 | let bytes = str.lines().map(String::from).collect(); 188 | combinations.push(bytes); 189 | } else if arg == "" { 190 | combinations.push(vec![",".to_string()]); 191 | } else { 192 | let replaced = arg.replace("??", "?").replace("//", "/"); 193 | combinations.push(vec![replaced.to_string()]); 194 | } 195 | } 196 | Ok(Dictionary::new(combinations, arg)?) 197 | } 198 | 199 | fn mask(arg: &str, charsets: &UserCharsets) -> Result { 200 | let arg = arg.replace("//", "/").replace(",,", ","); 201 | let mut example_start = vec![]; 202 | let mut example_end = vec![]; 203 | let wildcards = wildcards(charsets)?; 204 | let mut question = false; 205 | let mut combinations = 1_u64; 206 | for c in arg.chars() { 207 | if question { 208 | let wildcard = wildcards.get(&c).ok_or(Self::wildcard_err(c, &wildcards))?; 209 | example_start.push(wildcard.example_start.clone()); 210 | example_end.push(wildcard.example_end.clone()); 211 | combinations = combinations.saturating_mul(wildcard.length); 212 | question = false; 213 | } else if c == '?' { 214 | question = true; 215 | } else { 216 | example_start.push(c.to_string()); 217 | example_end.push(c.to_string()); 218 | } 219 | } 220 | if question { 221 | bail!("Mask '{}' ends in a ? use ?? to escape", arg); 222 | } 223 | Ok(Mask { 224 | arg, 225 | total: combinations, 226 | example_start: example_start.join(""), 227 | example_end: example_end.join(""), 228 | }) 229 | } 230 | 231 | fn wildcard_err(unknown: char, wildcards: &BTreeMap) -> Error { 232 | let mut valid = vec![]; 233 | for (c, wildcard) in wildcards { 234 | valid.push(format!(" ?{} - {}", c, wildcard.display)); 235 | } 236 | 237 | format_err!( 238 | "Wildcard '?{}' is unknown, valid wildcards are:\n{}", 239 | unknown, 240 | valid.join("\n") 241 | ) 242 | } 243 | } 244 | 245 | #[derive(Clone, Debug)] 246 | pub struct UserCharsets { 247 | charsets: BTreeMap, 248 | } 249 | 250 | impl UserCharsets { 251 | pub fn empty() -> Self { 252 | UserCharsets::new(vec![]).unwrap() 253 | } 254 | 255 | pub fn new(args: Vec>) -> Result { 256 | let mut charsets = BTreeMap::new(); 257 | for i in 0..args.len() { 258 | if let Some(str) = &args[i] { 259 | let num = i + 1; 260 | charsets.insert(num, Wildcard::new_custom(num, str)?); 261 | } 262 | } 263 | 264 | Ok(Self { charsets }) 265 | } 266 | 267 | pub fn add_binary_charsets(&mut self, entropy_bits: usize) -> Result> { 268 | let mut bin = vec![entropy_bits, 6, 5]; 269 | let mut totals = vec![2_u64.pow(entropy_bits as u32), 2_u64.pow(6), 2_u64.pow(5)]; 270 | let mut wildcards = vec![]; 271 | for i in 1..=4 { 272 | if self.charsets.contains_key(&i) { 273 | continue; 274 | } 275 | if let Some(bin) = bin.pop() { 276 | let total = totals.pop().unwrap(); 277 | let wildcard = Wildcard::new_binary(i, bin, total)?; 278 | self.charsets.insert(i, wildcard.clone()); 279 | wildcards.push(wildcard); 280 | } 281 | } 282 | Ok(wildcards) 283 | } 284 | 285 | pub fn to_wildcards(&self) -> Vec { 286 | self.charsets.iter().map(|(_, v)| v.clone()).collect() 287 | } 288 | } 289 | 290 | #[derive(Debug, Clone)] 291 | enum PassphraseArg { 292 | Dict(Dictionary), 293 | Mask(Mask), 294 | } 295 | 296 | #[derive(Debug, Clone)] 297 | pub struct Dictionary { 298 | combinations: Combinations, 299 | } 300 | 301 | impl Attempt for Dictionary { 302 | fn total(&self) -> u64 { 303 | self.combinations.total() 304 | } 305 | 306 | fn begin(&self) -> String { 307 | self.combinations.begin().join("") 308 | } 309 | 310 | fn end(&self) -> String { 311 | self.combinations.end().join("") 312 | } 313 | } 314 | 315 | impl Dictionary { 316 | pub fn new(vecs: Vec>, arg: &str) -> Result { 317 | let combinations = Combinations::new(vecs); 318 | if combinations.total() > MAX_DICT { 319 | bail!( 320 | "Dictionaries '{}' exceed 1B combinations\n Try splitting into 2 args or reducing size", 321 | arg 322 | ); 323 | } 324 | Ok(Self { combinations }) 325 | } 326 | } 327 | 328 | #[derive(Debug, Clone, Eq, PartialEq)] 329 | pub struct Mask { 330 | pub arg: String, 331 | total: u64, 332 | example_start: String, 333 | example_end: String, 334 | } 335 | 336 | impl Attempt for Mask { 337 | fn total(&self) -> u64 { 338 | self.total 339 | } 340 | 341 | fn begin(&self) -> String { 342 | self.example_start.clone() 343 | } 344 | 345 | fn end(&self) -> String { 346 | self.example_end.clone() 347 | } 348 | } 349 | 350 | impl Mask { 351 | pub fn empty() -> Self { 352 | Self::new("", 1, "", "") 353 | } 354 | 355 | fn prefix_wild(&mut self, wildcard: &Wildcard) { 356 | self.total = self.total.saturating_mul(wildcard.length); 357 | self.arg = format!("?{}{}", wildcard.flag, self.arg); 358 | } 359 | 360 | fn new(arg: &str, total: u64, start: &str, end: &str) -> Self { 361 | Self { 362 | arg: arg.to_string(), 363 | total, 364 | example_start: start.to_string(), 365 | example_end: end.to_string(), 366 | } 367 | } 368 | } 369 | 370 | #[derive(Debug, Clone, Eq, PartialEq)] 371 | pub struct Wildcard { 372 | flag: char, 373 | display: String, 374 | length: u64, 375 | example_start: String, 376 | example_end: String, 377 | charset: Option, 378 | } 379 | 380 | impl Wildcard { 381 | fn new(flag: char, display: &str, length: u64) -> Self { 382 | Self { 383 | flag, 384 | display: display.to_string(), 385 | length, 386 | example_start: display.chars().next().unwrap().to_string(), 387 | example_end: display.chars().last().unwrap().to_string(), 388 | charset: None, 389 | } 390 | } 391 | 392 | fn new_chars(flag: char, display: &str, length: u64, start: &str, end: &str) -> Self { 393 | Self { 394 | flag, 395 | display: display.to_string(), 396 | length, 397 | example_start: start.to_string(), 398 | example_end: end.to_string(), 399 | charset: None, 400 | } 401 | } 402 | 403 | fn new_binary(num: usize, bin: usize, total: u64) -> Result { 404 | let root1 = PathBuf::new() 405 | .join(HASHCAT_PATH) 406 | .join("charsets") 407 | .join("bin"); 408 | let root2 = PathBuf::new().join("charsets").join("bin"); 409 | 410 | for root in [root1.clone(), root2] { 411 | let path = root.join(format!("{}bit.hcchr", bin)); 412 | if path.exists() { 413 | let path = path.to_str().expect("Is valid path"); 414 | return Ok(Self { 415 | flag: num.to_string().chars().next().unwrap(), 416 | display: path.to_string(), 417 | length: total, 418 | example_start: "".to_string(), 419 | example_end: "".to_string(), 420 | charset: Some(path.to_string()), 421 | }); 422 | } 423 | } 424 | bail!("Could not find file '{:?}' make sure you are running in the directory with the '{}' folder", root1, HASHCAT_PATH); 425 | } 426 | 427 | fn new_custom(num: usize, display: &String) -> Result { 428 | if display.len() == 0 { 429 | bail!( 430 | "Custom charset {} is empty, pass in characters like so: -{} 'qwerty'", 431 | num, 432 | num 433 | ); 434 | } 435 | Ok(Self { 436 | flag: num.to_string().chars().next().unwrap(), 437 | display: format!("Custom charset '{}'", display), 438 | length: display.len() as u64, 439 | example_start: display.chars().next().unwrap().to_string(), 440 | example_end: display.chars().last().unwrap().to_string(), 441 | charset: Some(display.to_string()), 442 | }) 443 | } 444 | } 445 | 446 | fn wildcards(charsets: &UserCharsets) -> Result> { 447 | let mut wildcards = vec![ 448 | Wildcard::new('l', "abcdefghijklmnopqrstuvwxyz", 26), 449 | Wildcard::new('u', "ABCDEFGHIJKLMNOPQRSTUVWXYZ", 26), 450 | Wildcard::new('d', "0123456789", 10), 451 | Wildcard::new('h', "0123456789abcdef", 16), 452 | Wildcard::new('H', "0123456789ABCDEF", 16), 453 | Wildcard::new_chars( 454 | 's', 455 | "«space»!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~", 456 | 33, 457 | " ", 458 | "~", 459 | ), 460 | Wildcard::new_chars('a', "?l?u?d?s", 95, "a", "~"), 461 | Wildcard::new_chars('b', "0x00 - 0xFF", 256, "0x00", "0xFF"), 462 | Wildcard::new_chars('?', "Escapes '?' character", 1, "?", "?"), 463 | ]; 464 | wildcards.extend(charsets.to_wildcards()); 465 | 466 | let mut map = BTreeMap::new(); 467 | for wildcard in wildcards { 468 | map.insert(wildcard.flag, wildcard); 469 | } 470 | 471 | Ok(map) 472 | } 473 | 474 | #[cfg(test)] 475 | mod tests { 476 | use std::fs::remove_file; 477 | 478 | use crate::passphrase::*; 479 | 480 | #[tokio::test] 481 | async fn passphrase_generates_args() { 482 | let pp = Passphrase::from_arg(&vec!["?2".to_string()], &vec![None, Some("a".to_string())]); 483 | let args = pp.unwrap().build_args("hc", &Logger::off()).await; 484 | assert_eq!(args.unwrap(), vec!["-a", "3", "?2", "-2", "a"]); 485 | } 486 | 487 | fn bitfile(num: usize) -> String { 488 | let root = PathBuf::from_iter(vec!["hashcat", "charsets", "bin"].iter()); 489 | let bit = root.join(format!("{}bit.hcchr", num)); 490 | bit.into_os_string().into_string().unwrap() 491 | } 492 | 493 | #[tokio::test] 494 | async fn passphrase_can_add_binary_charsets() { 495 | let bit2 = bitfile(2); 496 | let bit3 = bitfile(3); 497 | let bit5 = bitfile(5); 498 | let bit6 = bitfile(6); 499 | 500 | let pp = Passphrase::from_arg( 501 | &vec!["test?d".to_string()], 502 | &vec![None, Some("a".to_string())], 503 | ) 504 | .unwrap(); 505 | let pp_with_bin = pp.add_binary_charsets(3, 2).unwrap().unwrap(); 506 | assert_args( 507 | pp_with_bin.build_args("", &Logger::off()).await, 508 | &format!( 509 | "-a 3 ?1?3?1?3?4test?d -1 {} -2 a -3 {} -4 {}", 510 | bit5, bit6, bit2 511 | ), 512 | ); 513 | assert_eq!(pp_with_bin.total(), 10 * 2048 * 2048 * 2_u64.pow(2)); 514 | 515 | let pp = Passphrase::from_arg(&vec!["test".to_string()], &vec![]).unwrap(); 516 | let pp_with_bin = pp.add_binary_charsets(3, 3).unwrap().unwrap(); 517 | assert_args( 518 | pp_with_bin.build_args("hc", &Logger::off()).await, 519 | &format!( 520 | "-a 7 ?1?2?1?2?3 hc_right.gz -1 {} -2 {} -3 {}", 521 | bit5, bit6, bit3 522 | ), 523 | ); 524 | assert_eq!(pp_with_bin.total(), 2048 * 2048 * 2_u64.pow(3)); 525 | remove_file("hc_right.gz").unwrap(); 526 | } 527 | 528 | fn assert_args(args: Result>, expected: &str) { 529 | let expected: Vec<_> = expected.split(" ").collect(); 530 | assert_eq!(args.unwrap(), expected); 531 | } 532 | 533 | #[test] 534 | fn can_add_binary_charsets() { 535 | let mut charsets = UserCharsets::new(vec![None, Some("a".to_string())]).unwrap(); 536 | let wildcards = charsets.add_binary_charsets(2).unwrap(); 537 | // Skips charset 2 because that's being used 538 | assert_eq!( 539 | wildcards, 540 | vec![ 541 | Wildcard::new_binary(1, 5, 2_u64.pow(5)).unwrap(), 542 | Wildcard::new_binary(3, 6, 2_u64.pow(6)).unwrap(), 543 | Wildcard::new_binary(4, 2, 2_u64.pow(2)).unwrap() 544 | ] 545 | ); 546 | let wildcards2 = charsets.add_binary_charsets(2).unwrap(); 547 | // Empty vec because there is no more charset space 548 | assert_eq!(wildcards2, vec![]); 549 | 550 | // The binary file 10 doesn't exist 551 | assert!(UserCharsets::empty().add_binary_charsets(10).is_err()); 552 | } 553 | 554 | #[test] 555 | fn validates_passphrase() { 556 | let pp = Passphrase::from_arg(&vec!["?2".to_string()], &vec![None, Some("a".to_string())]); 557 | assert_eq!(pp.unwrap().attack_mode, 3); 558 | 559 | let pp = Passphrase::from_arg(&vec!["asdf".to_string()], &vec![]); 560 | assert_eq!(pp.unwrap().attack_mode, 0); 561 | 562 | let pp = Passphrase::from_arg(&vec!["asdf".to_string(), "asdf".to_string()], &vec![]); 563 | assert_eq!(pp.unwrap().attack_mode, 1); 564 | 565 | let pp = Passphrase::from_arg(&vec!["?l".to_string(), "asdf".to_string()], &vec![]); 566 | assert_eq!(pp.unwrap().attack_mode, 7); 567 | 568 | let pp = Passphrase::from_arg(&vec!["asdf".to_string(), "?l".to_string()], &vec![]); 569 | assert_eq!(pp.unwrap().attack_mode, 6); 570 | 571 | let pp = Passphrase::from_arg(&vec!["?l".to_string(), "?l".to_string()], &vec![]); 572 | assert!(pp.is_err()); 573 | } 574 | 575 | #[test] 576 | fn validates_dicts() { 577 | let dict = Passphrase::dict("a,./dicts/10k.txt,,./dicts/10k_upper.txt,b").unwrap(); 578 | assert_eq!(dict.total(), 10_000 * 10_000); 579 | assert_eq!(dict.begin(), "athe,THEb".to_string()); 580 | assert_eq!(dict.end(), "apoison,POISONb".to_string()); 581 | 582 | let dict = 583 | Passphrase::dict("./dicts/1k.txt,.//,./dicts/1k_cap.txt,??,./dicts/1k_upper.txt") 584 | .unwrap(); 585 | assert_eq!(dict.total(), 1000 * 1000 * 1000); 586 | assert_eq!(dict.begin(), "the./The?THE".to_string()); 587 | assert_eq!(dict.end(), "entry./Entry?ENTRY".to_string()); 588 | 589 | assert!(Passphrase::dict("./dicts/asdf.txt").is_err()); 590 | assert!(Passphrase::dict("./dicts/100k.txt,./dicts/100k_cap.txt").is_err()); 591 | } 592 | 593 | fn charsets(chars: Vec<&str>) -> UserCharsets { 594 | let mut charsets = vec![]; 595 | for char in chars { 596 | if char.is_empty() { 597 | charsets.push(None); 598 | } else { 599 | charsets.push(Some(char.to_string())); 600 | } 601 | } 602 | UserCharsets::new(charsets).unwrap() 603 | } 604 | 605 | #[test] 606 | fn validates_masks() { 607 | let mask = Passphrase::mask("a?l//?d ??", &charsets(vec![])).unwrap(); 608 | assert_eq!(mask, Mask::new("a?l/?d ??", 260, "aa/0 ?", "az/9 ?")); 609 | 610 | let mask = Passphrase::mask("?b,,?1", &charsets(vec!["qwerty"])).unwrap(); 611 | assert_eq!(mask, Mask::new("?b,?1", 256 * 6, "0x00,q", "0xFF,y")); 612 | 613 | let mask = Passphrase::mask("?H ?2", &charsets(vec!["", "ab"])).unwrap(); 614 | assert_eq!(mask, Mask::new("?H ?2", 16 * 2, "0 a", "F b")); 615 | 616 | assert!(Passphrase::mask("?H ?2", &charsets(vec!["ab"])).is_err()); 617 | assert!(Passphrase::mask("?l?", &charsets(vec![])).is_err()); 618 | } 619 | } 620 | -------------------------------------------------------------------------------- /src/hashcat.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::fs::File; 3 | use std::io::{BufRead, BufReader, BufWriter, Write}; 4 | use std::path::{Path, PathBuf}; 5 | use std::process::{Child, ChildStderr, ChildStdin, Command, Stdio}; 6 | 7 | use anyhow::{format_err, Error, Result}; 8 | use crossterm::style::Stylize; 9 | use gzp::deflate::Gzip; 10 | use gzp::par::compress::{ParCompress, ParCompressBuilder}; 11 | use gzp::ZWriter; 12 | use tokio::spawn; 13 | use tokio::sync::mpsc::channel; 14 | use tokio::sync::mpsc::Receiver; 15 | use tokio::sync::mpsc::Sender; 16 | 17 | use crate::address::AddressValid; 18 | use crate::logger::{Attempt, Logger, Timer}; 19 | use crate::passphrase::Passphrase; 20 | use crate::seed::{Finished, Seed}; 21 | 22 | const DEFAULT_MAX_HASHES: u64 = 10_000_000; 23 | const DEFAULT_MIN_PASSPHRASES: u64 = 10_000; 24 | const HC_PID_FILE: &str = "hashcat.pid"; 25 | const HC_HASHES_FILE: &str = "_hashes.gz"; 26 | const HC_ERROR_FILE: &str = "_error.log"; 27 | const HC_OUTPUT_FILE: &str = "_output.log"; 28 | const CHANNEL_SIZE: usize = 100; 29 | const SEED_TASKS: usize = 1000; 30 | const STDIN_PASSPHRASE_MEM: usize = 10_000_000; 31 | const STDIN_BUFFER_BYTES: usize = 1000; 32 | const S_MODE_MAXIMUM: u64 = 100_000_000; 33 | 34 | /// Wrapper for the location of the exe 35 | #[derive(Debug, Clone)] 36 | pub struct HashcatExe { 37 | exe: PathBuf, 38 | } 39 | 40 | impl HashcatExe { 41 | pub fn new(exe: PathBuf) -> Self { 42 | Self { exe } 43 | } 44 | 45 | /// Move to seedcat folder for finding dicts 46 | fn cd_seedcat(&self) { 47 | let parent = self.exe.parent().expect("parent folder exists"); 48 | let parent = parent.parent().expect("parent folder exists"); 49 | env::set_current_dir(&parent).expect("can set dir"); 50 | } 51 | 52 | /// Move to hashcat folder for running 53 | fn cd_hashcat(&self) { 54 | let parent = self.exe.parent().expect("parent folder exists"); 55 | env::set_current_dir(&parent).expect("can set dir"); 56 | } 57 | 58 | /// Create command from the exe path 59 | fn command(&self) -> Command { 60 | Command::new(self.exe.clone()) 61 | } 62 | } 63 | 64 | /// Information about the hashcat mode 65 | #[derive(Debug, Clone)] 66 | pub struct HashcatMode { 67 | /// The kind of run we are doing 68 | pub runner: HashcatRunner, 69 | /// The number of passphrases we will try 70 | pub passphrases: u64, 71 | /// The number of hashes we write to the hashfile 72 | pub hashes: u64, 73 | } 74 | 75 | impl HashcatMode { 76 | fn new(runner: HashcatRunner, passphrases: u64, hashes: u64) -> Self { 77 | Self { 78 | runner, 79 | passphrases, 80 | hashes, 81 | } 82 | } 83 | 84 | /// True if we are running in pure GPU mode (instead of stdin) 85 | fn is_pure_gpu(&self) -> bool { 86 | match self.runner { 87 | HashcatRunner::StdinMaxHashes | HashcatRunner::StdinMinPassphrases => false, 88 | _ => true, 89 | } 90 | } 91 | } 92 | 93 | /// Represents how hashcat will be run 94 | #[derive(Debug, Clone)] 95 | pub enum HashcatRunner { 96 | /// Everything is run by hashcat itself 97 | PureGpu, 98 | /// Everything is run by hashcat itself using binary charsets in (Seed, Passphrase) 99 | BinaryCharsets(Seed, Passphrase), 100 | /// Running in stdin mode due to too many hashes 101 | StdinMaxHashes, 102 | /// Running in stdin mode due to too few passphrases 103 | StdinMinPassphrases, 104 | } 105 | 106 | /// Helper for running hashcat 107 | pub struct Hashcat { 108 | address: AddressValid, 109 | seed: Seed, 110 | passphrase: Option, 111 | pub max_hashes: u64, 112 | pub min_passphrases: u64, 113 | exe: HashcatExe, 114 | prefix: String, 115 | hashcat_args: Vec, 116 | total: u64, 117 | } 118 | 119 | impl Hashcat { 120 | pub fn new( 121 | exe: HashcatExe, 122 | address: AddressValid, 123 | seed: Seed, 124 | passphrase: Option, 125 | hashcat_args: Vec, 126 | ) -> Self { 127 | let mut total = seed.total(); 128 | total = total.saturating_mul(address.derivations.total()); 129 | if let Some(passphrase) = &passphrase { 130 | total = total.saturating_mul(passphrase.total()); 131 | } 132 | 133 | Self { 134 | exe, 135 | address, 136 | seed, 137 | passphrase, 138 | max_hashes: DEFAULT_MAX_HASHES, 139 | prefix: "hc".to_string(), 140 | min_passphrases: DEFAULT_MIN_PASSPHRASES, 141 | hashcat_args, 142 | total, 143 | } 144 | } 145 | 146 | /// Total guesses we will make 147 | pub fn total(&self) -> u64 { 148 | self.total 149 | } 150 | 151 | /// How all files will be prefixed (used for tests) 152 | pub fn set_prefix(&mut self, prefix: String) { 153 | self.prefix = prefix; 154 | } 155 | 156 | /// Get the mode we will run in 157 | pub fn get_mode(&self) -> Result { 158 | let total_derivations = self.address.derivations.args().len() as u64; 159 | let binary_charsets = self.seed.binary_charsets(self.max_hashes, &self.passphrase); 160 | if let Some((seed, passphrase)) = binary_charsets? { 161 | if passphrase.total() > self.min_passphrases { 162 | return Ok(HashcatMode::new( 163 | HashcatRunner::BinaryCharsets(seed.clone(), passphrase.clone()), 164 | passphrase.total(), 165 | seed.total_args() * total_derivations, 166 | )); 167 | } 168 | } 169 | let derivations = self.address.derivations.args().len() as u64; 170 | let passphrases = match &self.passphrase { 171 | None => 0, 172 | Some(passphrase) => passphrase.total(), 173 | }; 174 | 175 | let gpu_hashes = self.seed.valid_seeds() * derivations; 176 | let stdin_hashes = self.seed.total_args() * derivations; 177 | if gpu_hashes > self.max_hashes { 178 | let mode = HashcatMode::new(HashcatRunner::StdinMaxHashes, 0, stdin_hashes); 179 | return Ok(mode); 180 | } 181 | if passphrases < self.min_passphrases { 182 | let mode = HashcatMode::new(HashcatRunner::StdinMinPassphrases, 0, stdin_hashes); 183 | return Ok(mode); 184 | } 185 | let mode = HashcatMode::new(HashcatRunner::PureGpu, passphrases, gpu_hashes); 186 | Ok(mode) 187 | } 188 | 189 | /// Runs the hashcat program 190 | pub async fn run(&mut self, log: &Logger, is_bench: bool) -> Result<(Timer, Finished)> { 191 | self.exe.cd_hashcat(); 192 | 193 | // Required on windows for stdin mode 194 | File::create(HC_PID_FILE).expect("can create pid file"); 195 | 196 | let mut args = self.hashcat_args.clone(); 197 | args.push(self.hashfile()); 198 | 199 | let mut passphrase_args = vec![]; 200 | if let Some(passphrase) = &self.passphrase { 201 | passphrase_args = passphrase.build_args(&self.prefix, log).await?; 202 | } 203 | 204 | let mode = self.get_mode()?; 205 | let is_pure_gpu = mode.is_pure_gpu(); 206 | 207 | match mode.clone().runner { 208 | // All args get passed to hashcat, hashfile filled with valid seeds 209 | HashcatRunner::PureGpu => { 210 | for arg in &passphrase_args { 211 | args.push(arg.clone()); 212 | } 213 | self.seed = self.seed.with_pure_gpu(is_pure_gpu); 214 | let seed_rx = self.spawn_seed_senders().await; 215 | self.write_hashes(log, seed_rx, mode.hashes).await?; 216 | 217 | let child = self.spawn_hashcat(&args, mode); 218 | self.run_helper(child, log, is_bench).await 219 | } 220 | // All args get passed to hashcat, hashfile filled with args 221 | HashcatRunner::BinaryCharsets(seed, passphrase) => { 222 | for arg in &passphrase.build_args(&self.prefix, log).await? { 223 | args.push(arg.clone()); 224 | } 225 | self.passphrase = Some(passphrase); 226 | self.seed = seed.with_pure_gpu(is_pure_gpu); 227 | let rx = Self::spawn_arg_sender(&self.seed).await; 228 | self.write_hashes(log, rx, mode.hashes).await?; 229 | 230 | let child = self.spawn_hashcat(&args, mode); 231 | self.run_helper(child, log, is_bench).await 232 | } 233 | // Valid seeds and passphrases passed via stdin 234 | HashcatRunner::StdinMaxHashes | HashcatRunner::StdinMinPassphrases => { 235 | self.seed = self.seed.with_pure_gpu(is_pure_gpu); 236 | let seed_rx = self.spawn_seed_senders().await; 237 | let rx = Self::spawn_arg_sender(&self.seed).await; 238 | self.write_hashes(log, rx, mode.hashes).await?; 239 | 240 | let mut child = self.spawn_hashcat(&args, mode); 241 | let stdin = child.stdin.take(); 242 | let stdin = HashcatStdin::new(stdin, passphrase_args, &self.exe); 243 | spawn(Self::stdin_sender(self.prefix.clone(), stdin, seed_rx)); 244 | 245 | self.run_helper(child, log, is_bench).await 246 | } 247 | } 248 | } 249 | 250 | async fn run_helper( 251 | &self, 252 | mut child: Child, 253 | log: &Logger, 254 | is_bench: bool, 255 | ) -> Result<(Timer, Finished)> { 256 | // multiplier is how many derivations and seeds are performed per hash 257 | let mut multiplier = self.seed.hash_ratio(); 258 | multiplier *= self.address.derivations.hash_ratio(); 259 | let stderr = child.stderr.take(); 260 | spawn(Self::run_stderr(stderr, self.file(HC_ERROR_FILE)?)); 261 | let timer = log 262 | .time_verbose("Recovery Guesses", self.total(), multiplier as u64) 263 | .await; 264 | let result = self.run_stdout(child, log, &timer, is_bench).await?; 265 | let found = self.seed.found(result)?; 266 | self.exe.cd_seedcat(); 267 | Ok((timer, found)) 268 | } 269 | 270 | fn hashfile(&self) -> String { 271 | format!("{}{}", self.prefix, HC_HASHES_FILE) 272 | } 273 | 274 | async fn write_hashes( 275 | &self, 276 | log: &Logger, 277 | mut receiver: Receiver>, 278 | total: u64, 279 | ) -> Result<()> { 280 | let timer = log.time("Writing Hashes", total).await; 281 | let timer_handle = timer.start().await; 282 | let hashfile = self.hashfile(); 283 | let path = Path::new(&hashfile); 284 | let file = File::create(path).unwrap(); 285 | let writer = BufWriter::new(file); 286 | let address = &self.address; 287 | 288 | let mut parz: ParCompress = ParCompressBuilder::new().from_writer(writer); 289 | let kind = address.kind.key.as_bytes(); 290 | let separator = ":".as_bytes(); 291 | let address = address.formatted.as_bytes(); 292 | let newline = "\n".as_bytes(); 293 | 294 | while let Some(seed) = receiver.recv().await { 295 | for derivation in self.address.derivations.args() { 296 | parz.write_all(kind).map_err(Error::msg)?; 297 | parz.write_all(separator).map_err(Error::msg)?; 298 | parz.write_all(derivation.as_bytes()).map_err(Error::msg)?; 299 | parz.write_all(separator).map_err(Error::msg)?; 300 | parz.write_all(&seed).map_err(Error::msg)?; 301 | parz.write_all(separator).map_err(Error::msg)?; 302 | parz.write_all(address).map_err(Error::msg)?; 303 | parz.write_all(newline).map_err(Error::msg)?; 304 | timer.add(1); 305 | } 306 | } 307 | parz.finish().map_err(Error::msg)?; 308 | timer.end(); 309 | timer_handle.await.map_err(Error::msg) 310 | } 311 | 312 | fn spawn_hashcat(&self, args: &Vec, mode: HashcatMode) -> Child { 313 | let mut cmd = self.exe.command(); 314 | cmd.arg("-m"); 315 | cmd.arg("28510"); 316 | cmd.arg("-w"); 317 | cmd.arg("4"); 318 | cmd.arg("--status"); 319 | cmd.arg("--self-test-disable"); 320 | cmd.arg("--status-timer"); 321 | cmd.arg("1"); 322 | cmd.arg("--potfile-disable"); 323 | 324 | // FIXME: Tuning is needed for faster status updates 325 | cmd.arg("--force"); 326 | cmd.arg("-n"); 327 | cmd.arg("1"); 328 | 329 | // -S mode is faster if we have <10M passphrases 330 | if mode.is_pure_gpu() && mode.passphrases < S_MODE_MAXIMUM { 331 | let attack_mode = self 332 | .passphrase 333 | .clone() 334 | .map(|p| p.attack_mode) 335 | .unwrap_or_default(); 336 | if attack_mode != 6 && attack_mode != 7 { 337 | cmd.arg("-S"); 338 | } 339 | } 340 | for arg in args { 341 | cmd.arg(arg); 342 | } 343 | // println!("Running {:?}", cmd); 344 | 345 | cmd.stdin(Stdio::piped()) 346 | .stdout(Stdio::piped()) 347 | .stderr(Stdio::piped()) 348 | .spawn() 349 | .expect("Could not start hashcat process") 350 | } 351 | 352 | async fn stdin_sender(prefix: String, mut stdin: HashcatStdin, mut rx: Receiver>) { 353 | if stdin.passphrase_args.is_empty() { 354 | while let Some(seed) = rx.recv().await { 355 | stdin.stdin_send(seed); 356 | } 357 | } else { 358 | let mut pass_buffer = vec![]; 359 | while let Some(seed) = rx.recv().await { 360 | let mut pass_rx = Self::spawn_passphrases(&prefix, &stdin, &mut pass_buffer).await; 361 | for pass in &pass_buffer { 362 | let mut input = seed.clone(); 363 | input.extend_from_slice(pass); 364 | stdin.stdin_send(input); 365 | } 366 | while let Some(pass) = pass_rx.recv().await { 367 | let mut input = seed.clone(); 368 | input.extend(pass); 369 | stdin.stdin_send(input); 370 | } 371 | } 372 | } 373 | stdin.flush(); 374 | } 375 | 376 | async fn spawn_passphrases( 377 | prefix: &str, 378 | stdin: &HashcatStdin, 379 | buffer: &mut Vec>, 380 | ) -> Receiver> { 381 | let (tx, mut rx) = channel(CHANNEL_SIZE); 382 | 383 | // all passphrases fit in memory 384 | let buffer_len = buffer.len(); 385 | if buffer_len > 0 && buffer_len < STDIN_PASSPHRASE_MEM { 386 | return rx; 387 | } 388 | 389 | // spawn hashcat to stdout to generate passphrases 390 | let exe = stdin.exe.clone(); 391 | let passphrase_args = stdin.passphrase_args.clone(); 392 | let mut cmd = exe.command(); 393 | cmd.arg("--stdout"); 394 | cmd.arg("--session"); 395 | cmd.arg(format!("{}stdout", prefix)); 396 | 397 | for arg in passphrase_args { 398 | cmd.arg(arg); 399 | } 400 | spawn(async move { 401 | let child = cmd 402 | .stdout(Stdio::piped()) 403 | .stderr(Stdio::piped()) 404 | .spawn() 405 | .expect("Could not start hashcat process"); 406 | let out = child.stdout.expect("Pipes stdout"); 407 | 408 | let reader = BufReader::new(out); 409 | let mut num = 0; 410 | for read in reader.lines() { 411 | num += 1; 412 | if num > buffer_len { 413 | if tx.send(read.unwrap().into_bytes()).await.is_err() { 414 | break; 415 | } 416 | } 417 | } 418 | }); 419 | 420 | // initialize buffer 421 | if buffer.len() == 0 { 422 | while let Some(rx) = rx.recv().await { 423 | if buffer.len() == STDIN_PASSPHRASE_MEM { 424 | break; 425 | } 426 | buffer.push(rx); 427 | } 428 | } 429 | 430 | rx 431 | } 432 | 433 | fn file(&self, name: &str) -> Result> { 434 | let name = self.prefix.clone() + name; 435 | let path = Path::new(&name); 436 | let file = 437 | File::create(path).map_err(|_| format_err!("Unable to create file '{}'", name))?; 438 | Ok(BufWriter::new(file)) 439 | } 440 | 441 | async fn spawn_arg_sender(seed: &Seed) -> Receiver> { 442 | let (tx, rx) = channel(CHANNEL_SIZE); 443 | let mut seed = seed.clone(); 444 | spawn(async move { 445 | while let Some(arg) = seed.next_arg() { 446 | tx.send(arg.into_bytes()) 447 | .await 448 | .expect("Arg receiver stoppped"); 449 | } 450 | }); 451 | rx 452 | } 453 | 454 | async fn spawn_seed_senders(&self) -> Receiver> { 455 | let (tx, rx) = channel(CHANNEL_SIZE); 456 | for shard in self.seed.shard_words(SEED_TASKS) { 457 | spawn(Self::seed_sender(shard, tx.clone())); 458 | } 459 | rx 460 | } 461 | 462 | async fn seed_sender(mut seed: Seed, sender: Sender>) { 463 | while let Some(next) = seed.next_valid() { 464 | if sender.send(next).await.is_err() { 465 | // receiver thread was killed 466 | break; 467 | } 468 | } 469 | } 470 | 471 | async fn run_stdout( 472 | &self, 473 | mut child: Child, 474 | log: &Logger, 475 | timer: &Timer, 476 | is_bench: bool, 477 | ) -> Result> { 478 | let mut handle = None; 479 | 480 | let address = self.address.formatted.clone(); 481 | let mut file = self.file(HC_OUTPUT_FILE)?; 482 | let out = child.stdout.take().expect("Pipes stdout"); 483 | let address = format!("{}:", address); 484 | let reader = BufReader::new(out); 485 | log.println("Waiting for GPU initialization please be patient...".bold()); 486 | for read in reader.lines() { 487 | let line = read.map_err(Error::from)?; 488 | if line.contains("* Device") && !line.contains("WARNING") && !line.contains("skipped") { 489 | log.println(line.as_str().stylize()); 490 | } else if line.starts_with("Time.Started.....: ") && handle.is_none() { 491 | let num = line.split(" (").nth(1).unwrap(); 492 | let num = num.split(" sec").nth(0).unwrap(); 493 | let num = num.parse::().expect("is num"); 494 | handle = Some(timer.start_at(num).await); 495 | } else if line.starts_with("Progress.........: ") { 496 | let num = line.split(": ").nth(1).unwrap(); 497 | let num = num.split("/").nth(0).unwrap(); 498 | let total = num.parse::().expect("is num"); 499 | timer.store(total); 500 | } else if line.contains(&address) { 501 | child.kill().expect("can kill process"); 502 | timer.end(); 503 | if let Some(handle) = handle { 504 | handle.await.expect("Logging finishes"); 505 | } 506 | return Ok(line.split(":").nth(1).map(ToString::to_string)); 507 | } else if is_bench && timer.seconds() >= 60 { 508 | break; 509 | } 510 | writeln!(file, "{}", line).map_err(Error::from)?; 511 | file.flush().map_err(Error::from)?; 512 | } 513 | child.kill().expect("can kill process"); 514 | timer.end(); 515 | if let Some(handle) = handle { 516 | handle.await.expect("Logging finishes"); 517 | } 518 | Ok(None) 519 | } 520 | 521 | async fn run_stderr(err: Option, mut file: BufWriter) -> Result<()> { 522 | let err = err.expect("Piped stderr"); 523 | let reader = BufReader::new(err); 524 | for read in reader.lines() { 525 | let line = read.map_err(Error::from)?; 526 | writeln!(file, "{}", line).map_err(Error::from)?; 527 | file.flush().map_err(Error::from)?; 528 | } 529 | Ok(()) 530 | } 531 | } 532 | 533 | struct HashcatStdin { 534 | stdin: ChildStdin, 535 | stdin_buffer: Vec, 536 | passphrase_args: Vec, 537 | exe: HashcatExe, 538 | } 539 | 540 | impl HashcatStdin { 541 | pub fn new(stdin: Option, passphrase_args: Vec, exe: &HashcatExe) -> Self { 542 | Self { 543 | stdin: stdin.expect("Stdin piped"), 544 | stdin_buffer: vec![], 545 | passphrase_args, 546 | exe: exe.clone(), 547 | } 548 | } 549 | 550 | fn stdin_send(&mut self, pass: Vec) { 551 | self.stdin_buffer.extend(pass); 552 | self.stdin_buffer.push(10); // terminate password 553 | if self.stdin_buffer.len() > STDIN_BUFFER_BYTES { 554 | // might close when we find a match 555 | let _ = self.stdin.write_all(&self.stdin_buffer); 556 | self.stdin_buffer.clear(); 557 | } 558 | } 559 | 560 | pub fn flush(&mut self) { 561 | // might close early due to success 562 | let _ = self.stdin.write_all(&self.stdin_buffer); 563 | let _ = self.stdin.flush(); 564 | } 565 | } 566 | 567 | #[cfg(test)] 568 | mod tests { 569 | use crate::hashcat::*; 570 | 571 | fn hashcat(passphrase: &str, seed: &str) -> Hashcat { 572 | let passphrase = Passphrase::from_arg(&vec![passphrase.to_string()], &vec![]).unwrap(); 573 | let seed = Seed::from_args(seed, &None).unwrap(); 574 | let derivation = Some("m/0/0".to_string()); 575 | let address = 576 | AddressValid::from_arg("1B2hrNm7JGW6Wenf8oMvjWB3DPT9H9vAJ9", &derivation).unwrap(); 577 | Hashcat::new( 578 | HashcatExe::new(PathBuf::new()), 579 | address, 580 | seed.clone(), 581 | Some(passphrase.clone()), 582 | vec![], 583 | ) 584 | } 585 | 586 | #[test] 587 | fn determines_whether_to_run_pure_gpu() { 588 | let hc = hashcat("", "zoo,zoo,zoo,zoo,zoo,zoo,zoo,zoo,zoo,zoo,?,?"); 589 | let mode = hc.get_mode().unwrap(); 590 | assert!(matches!(mode.runner, HashcatRunner::BinaryCharsets(_, _))); 591 | assert_eq!(mode.hashes, 1); 592 | assert_eq!(mode.passphrases, 2048 * 2_u64.pow(7)); 593 | 594 | let hc = hashcat("", "zoo,zoo,zoo,zoo,zoo,zoo,zoo,zoo,zoo,zoo,z?,?"); 595 | let mode = hc.get_mode().unwrap(); 596 | assert!(matches!(mode.runner, HashcatRunner::StdinMinPassphrases)); 597 | assert_eq!(mode.hashes, 1); 598 | assert_eq!(mode.passphrases, 0); 599 | 600 | let hc = hashcat("?d?d?d?d", "zoo,zoo,zoo,zoo,zoo,zoo,zoo,zoo,zoo,zoo,z?,?"); 601 | let mode = hc.get_mode().unwrap(); 602 | assert!(matches!(mode.runner, HashcatRunner::BinaryCharsets(_, _))); 603 | assert_eq!(mode.hashes, 4); // number of z words 604 | assert_eq!(mode.passphrases, 10_000 * 2_u64.pow(7)); 605 | 606 | let hc = hashcat("?d?d?d?d", "?,?,zoo,zoo,zoo,zoo,zoo,zoo,zoo,zoo,zoo,zoo"); 607 | let mode = hc.get_mode().unwrap(); 608 | assert!(matches!(mode.runner, HashcatRunner::PureGpu)); 609 | assert_eq!(mode.hashes, (2048 * 2048) / 16); // valid seeds estimate 610 | assert_eq!(mode.passphrases, 10_000); 611 | 612 | let hc = hashcat("?d?d", "?,?,zoo,zoo,zoo,zoo,zoo,zoo,zoo,zoo,zoo,zoo"); 613 | let mode = hc.get_mode().unwrap(); 614 | assert!(matches!(mode.runner, HashcatRunner::StdinMinPassphrases)); 615 | assert_eq!(mode.hashes, 1); 616 | assert_eq!(mode.passphrases, 0); 617 | 618 | let hc = hashcat("?d?d?d?d", "?,?,?,zoo,zoo,zoo,zoo,zoo,zoo,zoo,zoo,zoo"); 619 | let mode = hc.get_mode().unwrap(); 620 | assert!(matches!(mode.runner, HashcatRunner::StdinMaxHashes)); 621 | assert_eq!(mode.hashes, 1); 622 | assert_eq!(mode.passphrases, 0); 623 | assert_eq!(hc.total(), 10_000 * 2048 * 2048 * 2048); 624 | } 625 | } 626 | --------------------------------------------------------------------------------