10 Commits
Author SHA1 Message Date
nelsbrock c6b5020795 release version 0.3.0 2026-05-09 16:30:04 +02:00
nelsbrock c74dcd0f01 add --userid option 2026-05-09 16:27:59 +02:00
nelsbrock 06e528ac09 upgrade and update dependencies 2026-05-09 16:27:32 +02:00
nelsbrock 890d8fa0be release version 0.2.2 2026-04-04 17:11:43 +02:00
nelsbrock 112a6b087b upgrade dependencies 2026-04-04 17:05:36 +02:00
nelsbrock fe984f2e43 release version 0.2.1 2025-11-01 09:42:39 +01:00
nelsbrock b1450070d1 use a bounded channel and fix panic in race condition 2025-11-01 09:40:42 +01:00
nelsbrock 241fecd8a7 release version 0.2.0 2025-10-25 19:32:59 +02:00
nelsbrock 273c1059a2 fix README.md 2025-10-25 19:28:58 +02:00
nelsbrock 1f659c33a7 stop gracefully on ctrl-c 2025-10-25 18:54:40 +02:00
5 changed files with 687 additions and 510 deletions
Generated
+556 -433
View File
File diff suppressed because it is too large Load Diff
+7 -6
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "fingerprunk" name = "fingerprunk"
version = "0.1.0" version = "0.3.0"
authors = ["Niklas Elsbrock <mail@nelsbrock.de>"] authors = ["Niklas Elsbrock <mail@nelsbrock.de>"]
edition = "2024" edition = "2024"
description = "CLI tool for brute-forcing OpenPGP keys with cool fingerprints" description = "CLI tool for brute-forcing OpenPGP keys with cool fingerprints"
@@ -10,10 +10,11 @@ keywords = ["fingerprint", "openpgp", "bruteforce"]
categories = ["command-line-utilities"] categories = ["command-line-utilities"]
[dependencies] [dependencies]
anyhow = "1.0.100" anyhow = "1.0.102"
clap = { version = "4.5.50", features = ["derive"] } clap = { version = "4.6.1", features = ["derive"] }
fancy-regex = "0.16.2" ctrlc = "3.5.2"
fancy-regex = "0.18.0"
num-integer = "0.1.46" num-integer = "0.1.46"
num_cpus = "1.17.0" num_cpus = "1.17.0"
rpassword = "7.4.0" rpassword = "7.5.2"
sequoia-openpgp = "2.0.0" sequoia-openpgp = "2.2.0"
+10 -10
View File
@@ -18,11 +18,11 @@ at `secret.asc`. The regex for this is `^C0FFEE`. Now, simply use the following
the search: the search:
```sh ```sh
fingerprunk -r '^C0FFEE' >> secret.asc fingerprunk -r '^C0FFEE' -u "Your Name <your.email@example.org>" >> secret.asc
``` ```
Fingerprunk will now generate many keys and write out all keys with matching fingerprints to Fingerprunk will now generate many keys and write out all keys with matching fingerprints to
standard output (here: `secret.asc`). standard output (here: `secret.asc`), adding the provided user ID.
If you want Fingerprunk to output password-encrypted keys use the `-p` flag and you will be prompted If you want Fingerprunk to output password-encrypted keys use the `-p` flag and you will be prompted
for a password. for a password.
@@ -51,7 +51,7 @@ Also see <https://en.wikipedia.org/wiki/Hexspeak> for some further examples of "
### How long does it take? ### How long does it take?
On my machine with an AMD Ryzen 7 5800X processor, Fingerprunk is able to generate and check about On my machine with an AMD Ryzen 7 5800X processor, Fingerprunk is able to generate and check about
41000 keys per second. This means that for finding a fingerprint with a string of *n* specific 43500 keys per second. This means that for finding a fingerprint with a string of *n* specific
hexadecimal digits at a specific place, I could expect the following runtimes until finding the hexadecimal digits at a specific place, I could expect the following runtimes until finding the
first key: first key:
@@ -60,15 +60,15 @@ first key:
| 1 | 16 = 16¹ | < 0.1 secs | | 1 | 16 = 16¹ | < 0.1 secs |
| 2 | 256 = 16² | < 0.1 secs | | 2 | 256 = 16² | < 0.1 secs |
| 3 | 4096 = 16³ | 0.1 secs | | 3 | 4096 = 16³ | 0.1 secs |
| 4 | 65536 = 16⁴ | 1.6 secs | | 4 | 65536 = 16⁴ | 1.5 secs |
| 5 | 1048576 = 16⁵ | 26 secs | | 5 | 1048576 = 16⁵ | 24 secs |
| 6 | 16777216 = 16⁶ | 7 mins | | 6 | 16777216 = 16⁶ | 6 mins |
| 7 | 268435456 = 16⁷ | 2 hours | | 7 | 268435456 = 16⁷ | 2 hours |
| 8 | 4294967296 = 16⁸ | 1 days | | 8 | 4294967296 = 16⁸ | 1 days |
| 9 | 68719476736 = 16⁹ | 19 days | | 9 | 68719476736 = 16⁹ | 18 days |
| 10 | 1099511627776 = 16¹⁰ | 310 days | | 10 | 1099511627776 = 16¹⁰ | 293 days |
| 11 | 17592186044416 = 16¹¹ | 14 years | | 11 | 17592186044416 = 16¹¹ | 13 years |
| 12 | 281474976710656 = 16¹² | 218 years | | 12 | 281474976710656 = 16¹² | 205 years |
As you can see, anything above 10 fixed digits is pretty much unfeasible, at least with a normal As you can see, anything above 10 fixed digits is pretty much unfeasible, at least with a normal
personal computer. personal computer.
+92 -59
View File
@@ -9,7 +9,7 @@ use std::{
mpsc, mpsc,
}, },
thread, thread,
time::{Duration, Instant}, time::{Duration, Instant, SystemTime},
}; };
use fancy_regex::Regex; use fancy_regex::Regex;
@@ -18,7 +18,7 @@ use sequoia_openpgp::{
Cert, Packet, armor, Cert, Packet, armor,
crypto::Password, crypto::Password,
packet::{ packet::{
Key, Key, UserID,
key::{Key4, PrimaryRole, SecretParts}, key::{Key4, PrimaryRole, SecretParts},
prelude::SignatureBuilder, prelude::SignatureBuilder,
}, },
@@ -28,12 +28,19 @@ use sequoia_openpgp::{
type SecretKey = Key<SecretParts, PrimaryRole>; type SecretKey = Key<SecretParts, PrimaryRole>;
#[allow(clippy::large_enum_variant)]
enum Message {
Key(SecretKey),
Stop,
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Config { pub struct Config {
pub regex: Regex, pub regex: Regex,
pub status_enabled: bool, pub status_enabled: bool,
pub stop_after: Option<NonZeroU64>, pub stop_after: Option<NonZeroU64>,
pub password: Option<Password>, pub password: Option<Password>,
pub userids: Vec<UserID>,
} }
#[derive(Debug)] #[derive(Debug)]
@@ -63,54 +70,71 @@ impl Fingerprunk {
} }
} }
pub fn run(mut self) { pub fn run(mut self) -> anyhow::Result<()> {
self.started_instant = Instant::now(); self.started_instant = Instant::now();
let (tx, rx) = mpsc::channel(); let (sender, receiver) = mpsc::sync_channel(16);
{
let sender = sender.clone();
ctrlc::set_handler(move || {
let _ = sender.send(Message::Stop);
})?;
}
thread::scope(|scope| { thread::scope(|scope| {
const THREAD_SPAWN_EXPECT_MSG: &str = "should be able to spawn thread";
let ref_self = &self; let ref_self = &self;
let status_displayer = if self.config.status_enabled { let status_displayer = if self.config.status_enabled {
Some( Some(
thread::Builder::new() thread::Builder::new()
.name("status_displayer".to_string()) .name("status_displayer".to_string())
.spawn_scoped(scope, move || ref_self.status_displayer_thread()) .spawn_scoped(scope, move || ref_self.status_displayer_thread())?,
.expect(THREAD_SPAWN_EXPECT_MSG),
) )
} else { } else {
None None
}; };
for num in 0..num_cpus::get() { for num in 0..num_cpus::get() {
let tx = tx.clone(); let sender = sender.clone();
thread::Builder::new() thread::Builder::new()
.name(format!("worker-{num:03}")) .name(format!("worker-{num:03}"))
.spawn_scoped(scope, move || ref_self.worker_thread(tx)) .spawn_scoped(scope, move || ref_self.worker_thread(sender))?;
.expect(THREAD_SPAWN_EXPECT_MSG);
} }
let on_stop = || { let mut stdout = io::stdout().lock();
// Ask all other threads to stop
self.stop.store(true, Ordering::Relaxed);
// Unpark the status displayer thread // Receive and process messages from the workers and the ctrl-c handler
if let Some(status_displayer) = status_displayer { for message in receiver {
status_displayer.thread().unpark(); match message {
Message::Key(key) => {
let cert = self.key_to_cert(&key)?;
self.serialize_cert(cert, &mut stdout)?;
// Increase "found" counter and stop if enough matches have been found
let prev = self.counter_found.fetch_add(1, Ordering::Relaxed);
if self.config.stop_after.is_some_and(|s| prev + 1 == s.get()) {
break;
}
}
Message::Stop => break,
} }
}; }
thread::Builder::new() // Ask all other threads to stop
.name("finalizer".to_string()) self.stop.store(true, Ordering::Relaxed);
.spawn_scoped(scope, move || ref_self.finalizer_thread(rx, on_stop))
.expect(THREAD_SPAWN_EXPECT_MSG); // Unpark the status displayer thread, if existant
}); if let Some(status_displayer) = status_displayer {
status_displayer.thread().unpark();
}
Ok(())
})
} }
fn worker_thread(&self, matches_tx: mpsc::Sender<SecretKey>) { fn worker_thread(&self, sender: mpsc::SyncSender<Message>) {
let mut fingerprint_hex = String::with_capacity(20 * 2); let mut fingerprint_hex = String::with_capacity(20 * 2);
while !self.stop.load(Ordering::Relaxed) { while !self.stop.load(Ordering::Relaxed) {
@@ -120,9 +144,9 @@ impl Fingerprunk {
write!(fingerprint_hex, "{:X}", key.fingerprint()) write!(fingerprint_hex, "{:X}", key.fingerprint())
.expect("should write into string without error"); .expect("should write into string without error");
if self.check_fingerprint(&fingerprint_hex) { if self.check_fingerprint(&fingerprint_hex) {
matches_tx // The channel might already be closed here if we're stopping.
.send(Key::V4(key)) // That is fine, so we just ignore the error.
.expect("should be able to send key"); let _ = sender.send(Message::Key(Key::V4(key)));
} }
self.counter_tried.fetch_add(1, Ordering::Relaxed); self.counter_tried.fetch_add(1, Ordering::Relaxed);
} }
@@ -136,43 +160,20 @@ impl Fingerprunk {
.expect("should check regex without error") .expect("should check regex without error")
} }
fn finalizer_thread(&self, matches_rx: mpsc::Receiver<SecretKey>, on_stop: impl FnOnce()) {
let mut stdout = io::stdout().lock();
for key in matches_rx {
let cert = self
.key_to_cert(&key)
.expect("should be able to create certificate");
self.serialize_cert(cert, &mut stdout)
.expect("should be able to serialize certificate");
let prev = self.counter_found.fetch_add(1, Ordering::Relaxed);
if self.config.stop_after.is_some_and(|s| prev + 1 == s.get()) {
break;
}
}
on_stop();
}
fn key_to_cert(&self, key: &SecretKey) -> anyhow::Result<Cert> { fn key_to_cert(&self, key: &SecretKey) -> anyhow::Result<Cert> {
let sig = SignatureBuilder::new(SignatureType::DirectKey) let creation_time = SystemTime::now();
.set_hash_algo(HashAlgorithm::SHA512)
.set_preferred_hash_algorithms(vec![HashAlgorithm::SHA512, HashAlgorithm::SHA256])?
.set_preferred_symmetric_algorithms(vec![
SymmetricAlgorithm::AES256,
SymmetricAlgorithm::AES128,
])?;
let mut signer = key let mut signer = key
.clone() .clone()
.into_keypair() .into_keypair()
.expect("key should have a secret"); .expect("key should have a secret");
let sig = sig.sign_direct_key(&mut signer, key.parts_as_public())?;
let secret_key_packet = Packet::SecretKey({ // Sign keypair
let key_sig = create_sig_builder(SignatureType::DirectKey, creation_time)?
.sign_direct_key(&mut signer, key.parts_as_public())?;
// Create certificate
let mut cert = Cert::try_from(Packet::SecretKey({
let mut key = key.clone(); let mut key = key.clone();
if let Some(ref password) = self.config.password { if let Some(ref password) = self.config.password {
let (k, mut secret) = key.take_secret(); let (k, mut secret) = key.take_secret();
@@ -180,9 +181,27 @@ impl Fingerprunk {
key = k.add_secret(secret).0; key = k.add_secret(secret).0;
} }
key key
}); }))?;
Cert::try_from(vec![secret_key_packet, Packet::from(sig)]) let mut packets = vec![Packet::from(key_sig)];
// Sign user IDs
let mut next_is_primary = true;
for user_id in self.config.userids.iter().cloned() {
let mut sig_builder =
create_sig_builder(SignatureType::PositiveCertification, creation_time)?;
if next_is_primary {
sig_builder = sig_builder.set_primary_userid(true)?;
next_is_primary = false;
}
let sig = user_id.bind(&mut signer, &cert, sig_builder)?;
packets.push(user_id.into());
packets.push(sig.into());
}
cert = cert.insert_packets(packets)?.0;
Ok(cert)
} }
fn serialize_cert(&self, cert: Cert, to: impl io::Write) -> anyhow::Result<()> { fn serialize_cert(&self, cert: Cert, to: impl io::Write) -> anyhow::Result<()> {
@@ -254,3 +273,17 @@ impl Fingerprunk {
); );
} }
} }
fn create_sig_builder(
typ: SignatureType,
creation_time: SystemTime,
) -> Result<SignatureBuilder, anyhow::Error> {
SignatureBuilder::new(typ)
.set_signature_creation_time(creation_time)?
.set_hash_algo(HashAlgorithm::SHA512)
.set_preferred_hash_algorithms(vec![HashAlgorithm::SHA512, HashAlgorithm::SHA256])?
.set_preferred_symmetric_algorithms(vec![
SymmetricAlgorithm::AES256,
SymmetricAlgorithm::AES128,
])
}
+22 -2
View File
@@ -7,6 +7,7 @@ use anyhow::{Context as AnyhowContext, anyhow};
use clap::{ArgAction, Parser, ValueEnum}; use clap::{ArgAction, Parser, ValueEnum};
use fancy_regex::Regex; use fancy_regex::Regex;
use fingerprunk::Fingerprunk; use fingerprunk::Fingerprunk;
use sequoia_openpgp::packet::UserID;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(version, about, long_about = None)] #[command(version, about, long_about = None)]
@@ -33,12 +34,22 @@ struct Args {
#[arg(long)] #[arg(long)]
stop_after: Option<NonZeroU64>, stop_after: Option<NonZeroU64>,
/// Prompt for a password and use it to encrypt found keys. /// Prompt for a password and use it to encrypt matching keys.
/// ///
/// By default, found keys are printed to stdout unencrypted. Use this if you actually plan to /// By default, found keys are printed to stdout unencrypted. Use this if you actually plan to
/// use generated keys. /// use generated keys.
#[arg(short, long, action = ArgAction::SetTrue)] #[arg(short, long, action = ArgAction::SetTrue)]
password: bool, password: bool,
/// Add the given user ID to matching keys.
#[arg(short, long = "userid")]
userid: Vec<UserID>,
/// Explicitly do not add user IDs to matching keys.
///
/// Disables the warning about importing keys without user IDs into GnuPG.
#[arg(long, conflicts_with = "userid", action = ArgAction::SetTrue)]
no_userid: bool,
} }
#[derive(ValueEnum, Clone, Copy, Debug, Default)] #[derive(ValueEnum, Clone, Copy, Debug, Default)]
@@ -62,6 +73,14 @@ impl StatusEnabled {
fn main() -> anyhow::Result<()> { fn main() -> anyhow::Result<()> {
let args = Args::parse(); let args = Args::parse();
if !args.no_userid && args.userid.is_empty() {
eprintln!(
"WARNING: No user ID was provided.\n\
You may experience problems importing generated keys into GnuPG.\n\
Use the --userid option to add a user ID.\n"
)
}
let password = if args.password { let password = if args.password {
let password = rpassword::prompt_password( let password = rpassword::prompt_password(
"Enter password for encrypting found keys (leave empty for no encryption): ", "Enter password for encrypting found keys (leave empty for no encryption): ",
@@ -87,9 +106,10 @@ fn main() -> anyhow::Result<()> {
status_enabled: args.status.evaluate(), status_enabled: args.status.evaluate(),
stop_after: args.stop_after, stop_after: args.stop_after,
password, password,
userids: args.userid,
}; };
Fingerprunk::new_from_config(config).run(); Fingerprunk::new_from_config(config).run()?;
Ok(()) Ok(())
} }