Compare commits

...

15 Commits
v1.0.0 ... main

4 changed files with 109 additions and 113 deletions

19
Cargo.lock generated
View File

@ -480,7 +480,6 @@ dependencies = [
"iana-time-zone",
"js-sys",
"num-traits",
"time",
"wasm-bindgen",
"windows-targets 0.48.5",
]
@ -1243,7 +1242,6 @@ name = "git-vain"
version = "1.0.0"
dependencies = [
"anyhow",
"chrono",
"clap",
"data-encoding",
"directories",
@ -2826,17 +2824,6 @@ dependencies = [
"syn 2.0.29",
]
[[package]]
name = "time"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a"
dependencies = [
"libc",
"wasi 0.10.0+wasi-snapshot-preview1",
"winapi",
]
[[package]]
name = "tiny-keccak"
version = "2.0.2"
@ -3048,12 +3035,6 @@ version = "0.9.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
[[package]]
name = "wasi"
version = "0.10.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"

View File

@ -16,7 +16,6 @@ crypto-rust = [
[dependencies]
anyhow = "1"
chrono = "0.4"
clap = { version = "4", features = ["derive"] }
data-encoding = "2"
directories = "5"

View File

@ -10,21 +10,39 @@ Supports:
- multithreading
- config file for saving common options
## Install
Download the [latest release][] or build from source using `cargo`.
Make sure `git-vain` is in your `$PATH` or running `git vain` won't work.
To build from source, run `cargo install --path .` or `cargo build --release`
in your local checkout.
[latest release]: https://git.anna.lgbt/anna/git-vain/releases/latest
## Usage
Replace the hash of the commit at `HEAD` with one that begins with `c0ffee`:
`git vain c0ffee`
Try using `--help` for more information.
## Performance
Performance depends on hardware, method, and number of threads. The tests below
were performed on Framework Laptop (A6) with an 11th Gen Intel® Core™ i7-1165G7
@ 2.80GHz. Each method was tested attempting to generate the prefix `deadbeef`
for 30 seconds using one thread.
were performed on an AMD Ryzen 7 3800X 8-Core Processor. Each result is the
average of three trials running for 30 seconds using a single thread.
```
counter: 163,504,123 hashes (5,461,682.8102/s)
header: 124,617,875 hashes (4,150,993.2260/s)
random: 110,886,440 hashes (3,715,452.2177/s)
increment: 107,137,532 hashes (3,573,438.8338/s)
sequoia*: 864,486 hashes ( 28,923.1870/s)
sequoia: 339,407 hashes ( 11,403.6102/s)
gpg-agent: 353 hashes ( 11.7519/s)
counter: 154,293,195.67 hashes (5,143,106.52/s)
random: 134,886,025.00 hashes (4,496,200.83/s)
header: 131,035,925.67 hashes (4,367,864.19/s)
increment: 95,501,212.00 hashes (3,183,373.73/s)
decrement: 95,358,885.33 hashes (3,178,629.51/s)
sequoia*: 839,872.67 hashes ( 27,995.76/s)
sequoia: 330,972.67 hashes ( 11,032.42/s)
gpg-agent: 113.67 hashes ( 3.79/s)
```
<small>The asterisked sequoia is using the `crypto-rust` feature.</small>

View File

@ -14,32 +14,19 @@ use git2::Repository;
use gpgme::{Context, Protocol, SignMode};
use indicatif::ProgressStyle;
use keyring::Entry;
use openpgp::{
packet::prelude::*,
parse::Parse,
serialize::{
Serialize,
stream::{Armorer, Message},
},
types::*,
};
use rand::Rng;
use sequoia_openpgp as openpgp;
use sequoia_openpgp::crypto::KeyPair;
use sequoia_openpgp::policy::StandardPolicy;
use sequoia_openpgp::{
self as openpgp,
crypto::KeyPair,
parse::Parse,
policy::StandardPolicy,
serialize::stream::{Armorer, Message, Signer},
};
use serde::Deserialize;
use sha1::{Digest, Sha1};
/// A vanity commit hash generator that's fast, works with PGP signing, and can
/// be used on arbitrary commits (not just HEAD).
///
/// This works by decrementing the commit time one second and recalculating the
/// hash until a hash matching your selected prefix is found.
///
/// If using PGP signing, the timestamp remains unchanged. Every PGP signature is different, even on
/// the same data, so decrementing the timestamp is no longer required.
/// Providing a key file is orders of magnitude faster than providing a key ID,
/// since using a key ID goes through gpg-agent, which is very slow.
#[derive(Parser)]
#[command(author, version)]
struct Cli {
@ -386,6 +373,10 @@ fn main() -> Result<()> {
seq_key = Some(unlock_key_file(path, true, false).context("could not get keypair")?.0);
}
if (key_id.is_some() || seq_key.is_some()) && args.method.is_some() {
eprintln!("warn: method specified will be ignored: commit signing is enabled (try with --no-sign)");
}
// first open the repo and find the commit pointed to by HEAD
let repo = Repository::open(".")
.context("no git repository in this directory")?;
@ -465,7 +456,7 @@ fn main() -> Result<()> {
for _ in 0..threads {
let bar = bar.clone();
let mut seq_key = seq_key.clone();
let seq_key = seq_key.clone();
let key = key_id.cloned();
let counter = Arc::clone(&counter);
let found = Arc::clone(&found);
@ -499,107 +490,114 @@ fn main() -> Result<()> {
None => None,
};
let signing = gpg.is_some() || seq_key.is_some();
let mut signature_bytes = Vec::with_capacity(1024);
let mut sha1 = Sha1::default();
let mut buffer = itoa::Buffer::new();
let mut count_buffer = itoa::Buffer::new();
let mut random_bytes = [0; 16];
let mut random_hex = [0; 32];
let mut first = true;
while !found.load(Ordering::SeqCst) {
let append = if gpg.is_some() || seq_key.is_some() {
None
} else {
match method {
while !found.load(Ordering::Relaxed) {
let (mut header, append) = if !signing {
let append = match method {
Method::Random => {
let mut bytes = [0; 16];
rand::thread_rng().fill(&mut bytes);
Some(Cow::from(data_encoding::HEXLOWER.encode(&bytes)))
rand::thread_rng().fill(&mut random_bytes);
data_encoding::HEXLOWER.encode_mut(&random_bytes, &mut random_hex);
let random = unsafe {
std::str::from_utf8_unchecked(&random_hex)
};
Some(random)
}
Method::Counter => {
let count = counter.fetch_add(1, Ordering::SeqCst) + 1;
Some(Cow::from(count_buffer.format(count)))
let count = counter.fetch_add(1, Ordering::Relaxed) + 1;
Some(count_buffer.format(count))
}
_ => None,
}
};
if matches!(method, Method::Increment | Method::Decrement) {
let timestamp = match method {
Method::Decrement => timestamp.fetch_sub(1, Ordering::SeqCst) - 1,
Method::Increment => timestamp.fetch_add(1, Ordering::SeqCst) + 1,
_ => timestamp.load(Ordering::SeqCst),
};
author_parts[author_len - 2] = buffer.format(timestamp).to_owned();
committer_parts[committer_len - 2] = buffer.format(timestamp).to_owned();
if matches!(method, Method::Increment | Method::Decrement) {
let timestamp = match method {
Method::Decrement => timestamp.fetch_sub(1, Ordering::Relaxed) - 1,
Method::Increment => timestamp.fetch_add(1, Ordering::Relaxed) + 1,
_ => unreachable!(),
};
header_lines[author_idx] = author_parts.join(" ");
header_lines[committer_idx] = committer_parts.join(" ");
}
author_parts[author_len - 2] = buffer.format(timestamp).to_owned();
committer_parts[committer_len - 2] = buffer.format(timestamp).to_owned();
if method == Method::Header {
let count = counter.fetch_add(1, Ordering::SeqCst) + 1;
if first {
header_lines.insert(committer_idx + 1, String::with_capacity(16));
first = false;
header_lines[author_idx] = author_parts.join(" ");
header_lines[committer_idx] = committer_parts.join(" ");
}
let line = &mut header_lines[committer_idx + 1];
line.clear();
line.push_str("xvain ");
line.push_str(buffer.format(count));
}
if method == Method::Header {
let count = counter.fetch_add(1, Ordering::Relaxed) + 1;
if first {
header_lines.insert(committer_idx + 1, String::with_capacity(16));
first = false;
}
let mut header = match method {
// counter and random don't mutate the header, so don't
// allocate for it
Method::Counter | Method::Random => Cow::from(&stripped_header),
_ => Cow::from(header_lines.join("\n")),
let line = &mut header_lines[committer_idx + 1];
line.clear();
line.push_str("xvain ");
line.push_str(buffer.format(count));
}
let header = match method {
Method::Counter | Method::Random => Cow::from(&stripped_header),
_ => Cow::from(header_lines.join("\n")),
};
(header, append)
} else {
(Cow::from(&stripped_header), None)
};
// NOTE: don't need to handle append here, since we'll never be
// both appending *and* signing
if let Some(ctx) = &mut gpg {
let header = header.to_mut();
signature_bytes.clear();
let to_sign = format!("{header}\n{message}");
let mut output = Vec::new();
ctx.sign(SignMode::Detached, to_sign, &mut output)
ctx.sign(SignMode::Detached, to_sign, &mut signature_bytes)
.context("could not sign commit")
.unwrap(); // FIXME
let sig = String::from_utf8(output)
let sig = std::str::from_utf8(&signature_bytes)
.context("signature was not utf-8")
.unwrap(); // FIXME
let header = header.to_mut();
header.push_str("gpgsig");
for line in sig.trim().split('\n') {
header.push(' ');
header.push_str(line);
header.push('\n');
}
} else if let Some(key) = &mut seq_key {
let header = header.to_mut();
let to_sign = format!("{header}\n{message}");
let sig = SignatureBuilder::new(SignatureType::Binary)
.sign_message(key, to_sign)
.context("failed to sign message")
.unwrap(); // FIXME
let mut output = Vec::new();
let message = Message::new(&mut output);
let mut message = Armorer::new(message)
} else if let Some(key) = &seq_key {
signature_bytes.clear();
let msg = Message::new(&mut signature_bytes);
let msg = Armorer::new(msg)
.kind(openpgp::armor::Kind::Signature)
.build()
.context("failed to build pgp message")
.context("failed to build armorer")
.unwrap(); // FIXME
Packet::from(sig)
.serialize(&mut message)
.context("failed to serialise packet")
.unwrap(); // FIXME
message.finalize()
.context("could not finalise message")
let mut msg = Signer::new(msg, key.clone())
.detached()
.build()
.context("failed to build signer")
.unwrap(); // FIXME
let sig = String::from_utf8(output)
msg.write_all(header.as_bytes()).unwrap(); // FIXME
msg.write_all(&[b'\n']).unwrap(); // FIXME
msg.write_all(message.as_bytes()).unwrap(); // FIXME
msg.finalize().unwrap(); // FIXME
let sig = std::str::from_utf8(&signature_bytes)
.context("signature was not utf-8")
.unwrap(); // FIXME
let header = header.to_mut();
header.push_str("gpgsig");
for line in sig.trim().split('\n') {
header.push(' ');
@ -639,7 +637,7 @@ fn main() -> Result<()> {
}
}
found.store(true, Ordering::SeqCst);
found.store(true, Ordering::Relaxed);
let message = match append {
Some(append) => Cow::from(format!("{message}\n{append}\n")),
None => Cow::from(&message),