Why generate keys offline
When you use an exchange or an online wallet, someone else holds your private keys. If they get hacked, go bankrupt, or decide to freeze your account, your Bitcoin is gone. This has happened repeatedly throughout Bitcoin's history.
The Bitcoin community has a saying: not your keys, not your coins. If you don't personally control the private key, you don't truly own the Bitcoin.
But generating keys on a computer connected to the internet is also risky. Malware, keyloggers, clipboard hijackers, and compromised software can steal your private key the moment it's created, before you even know it exists.
The safest approach is simple: generate your key on a computer that has never been and will never be connected to the internet. Even if the machine were compromised, it would have no way to send your key to an attacker.
That's what this tool is for.
How cold storage works
1. Get an air-gapped machine
Use any computer that is disconnected from the internet. An old laptop with Wi-Fi disabled works well. A Raspberry Pi is another good option. The key requirement: no network connection, ever, while the machine has access to your private key.
2. Copy btc-keygen onto the machine
Download the binary on a separate, internet-connected computer. Verify the checksum. Copy it to a USB drive. Plug the USB drive into the air-gapped machine.
3. Run the tool
Open a terminal and run btc-keygen generate. The tool
prints two things:
- A Bitcoin address (starts with
bc1q): your receiving address. Like a bank account number: give it to people so they can send you Bitcoin. Safe to share publicly. - A private key in WIF format (starts with
KorL): "Wallet Import Format." Think of it as the password to your Bitcoin. Anyone who has this string can spend your funds. Keep it absolutely secret.
4. Write it down
Write both the address and the private key on paper, or stamp them into metal. Do not save them to a file. The tool does not save anything: once you close the terminal, the key exists only where you wrote it down.
5. Store it securely
Put the paper or metal in a safe, a safety deposit box, or another secure location. This is your cold storage. To receive Bitcoin, give someone the address. To spend it later, import the private key into a wallet.
Critical. The tool generates a fresh key every time you run it. If you lose your written copy, there is no way to recover the private key. No one can help you: not the tool, not the developers, not anyone. Write it down carefully and store it safely.
Using your address and private key
Receiving Bitcoin
Share your address (the bc1q… string) with
the sender. You can share it by text, email, or any other channel; the
address is public information and cannot be used to steal your Bitcoin.
Check whether Bitcoin has arrived using any public block explorer (for example, search your address on mempool.space or blockchair.com). You do not need your private key to check your balance, only to spend.
Spending Bitcoin
When you want to spend the Bitcoin stored at your address, you need to import your WIF private key into a wallet application:
- Install a Bitcoin wallet that supports WIF import, for example Electrum, BlueWallet, or Sparrow Wallet.
- Look for an option called Import, Sweep, or Import Private Key. Exact wording varies by wallet.
- Enter or scan the WIF string, the one starting with
KorL. - The wallet will recognize the key, find the associated address, and show your balance.
- You can now send Bitcoin from that address.
Important. When you import or sweep a private key, you are exposing it to that device. Only do this on a trusted device. After spending, if you want to continue using cold storage, generate a new keypair for the remaining funds rather than reusing the same key.
Import vs sweep
- Import: adds the key to the wallet. The wallet now controls that address directly. Funds stay at the same address.
- Sweep: moves all funds from the imported key into a new address controlled by the wallet. The old address ends up empty. Generally the safer option because the private key is used only once.
Where the randomness comes from
Your private key is 32 bytes taken from the operating system and used exactly as they arrive. No hashing, no mixing, no derivation. That makes the key precisely as strong, or as weak, as those 32 bytes, so the only question worth asking is where they come from and what could change them quietly.
The question became urgent in July 2026. One model of one vendor's hardware wallet was found to have shipped firmware whose build guard tested whether a configuration macro was defined rather than whether it was enabled. In affected builds a software generator seeded from a chip serial number and a timer stood in for the real source, and seed entropy fell from 128 bits to roughly 32. The firmware had been public since March 2021, and reading that source did not reveal it. Those details are as publicly reported, not something measured here. So the rest of this page names files, line numbers and commands.
The one call site
One place in btc-keygen asks the operating system for random bytes.
grep -rn getrandom src/ finds it, along with two doc comments
that mention it:
src/entropy.rs:27: getrandom::fill(dest).map_err(|e| EntropyError(format!("OS CSPRNG failed: {}", e)))
src/lib.rs:30://! - Entropy comes from the OS CSPRNG via [`getrandom`](https://docs.rs/getrandom).
src/keygen.rs:206:/// entropy comes from the operating system's CSPRNG (`getrandom` syscall on
That call is the body of OsEntropy::fill_bytes
(src/entropy.rs:26-28). generate() hardcodes
OsEntropy (src/keygen.rs:218-220); the injectable form
generate_with_entropy is pub(crate)
(src/keygen.rs:184), the whole entropy module is
pub(crate) (src/lib.rs:58), and nothing from it is
re-exported (src/lib.rs:64-69). No public API accepts an entropy
source.
There is no build.rs in this repository. One does run
during a build: secp256k1-sys 0.11.0 ships one to compile the vendored C
library. It emits no cargo::rustc-cfg of any kind, so no
build script picks the entropy backend.
The test doubles FixedEntropy and
FailingEntropy sit behind #[cfg(test)]
(src/entropy.rs:31, 37, 47, 61, 64). The incident above turned on a guard
that tested the wrong thing, so be exact about this one: it keys on the
crate being compiled as a test target, not on the release profile.
Measured: cargo test --release --locked --lib --no-run
produces an optimized binary in which strings -a finds both
fixed entropy exhausted and
simulated entropy failure. Neither string appears in a
release build of the tool. That test binary is not what the releases page
serves, and neither double reaches the library a downstream crate
links.
Two limits sit alongside that. One call site is not one way to get a
key: PrivateKey::from_bytes (src/keygen.rs:114) and
PrivateKey::from_hex (src/keygen.rs:144) are public, and a
key imported through either touches no entropy source at all. And the
count is measured, not enforced. Secp256k1::new() at
src/pubkey.rs:8 would begin drawing randomness of its own through a
second, differently versioned getrandom the moment anything in the
dependency graph enabled secp256k1's optional rand feature
(secp256k1-0.31.1/src/context.rs:212-220). It is off today:
cargo tree --locked --target all -i rand reports that it has
nothing to print. Cargo features are additive and nothing here pins
secp256k1's feature set, so that change would arrive without a warning.
What happens to the 32 bytes
They become the key verbatim. The buffer is filled in place at
src/keygen.rs:190, straight into the key's own storage. Nothing is folded
in, and nothing is reduced modulo anything. The tool's own test proves it
on a known input: test_full_pipeline_deterministic
(src/lib.rs:108) feeds 32 fixed bytes through the same
generate_with_entropy the OS path uses and asserts the
private key equals those bytes, then checks the WIF and address that
follow. Run it with
cargo test --locked --lib test_full_pipeline_deterministic,
and cross-check the printed form with
btc-keygen generate --from-hex <those bytes> --hex.
The draw sits inside a rejection loop (src/keygen.rs:187-196,
MAX_RETRIES = 32 at src/keygen.rs:224). If the 32 bytes are
not a valid secp256k1 scalar they are zeroized whole and a fresh 32 bytes
requested, which is roughly a 1 in 2128 event. Bytes are used
verbatim or discarded entirely. Nothing in between.
Using them verbatim is plumbing, not a security property. It means the key is exactly as strong as the bytes the operating system handed over, which is why the source is the only part of this worth arguing about.
With --from-hex (src/main.rs:86-100, reaching
PrivateKey::from_hex at src/keygen.rs:144) the OS random
number generator is never consulted. Bringing your own 32 bytes is a
supported path, and on it the security of the key depends entirely on how
you sourced them. Supply them on stdin:
btc-keygen generate --from-hex - < key.hex. Passing the
key as the argument value is deprecated and goes away in 0.4.0, because
an argument lands in your shell history and is visible to ps
while the command runs, and neither exposure can be undone afterwards
(src/main.rs:20-29, docs/01-threat-model.md T11).
Which backend each release target gets
Cargo.toml requests getrandom = "0.4", which is a range.
Cargo.lock pins 0.4.3 (Cargo.lock:207), and release builds consume it
with --locked, so everything below is a property of 0.4.3,
not of 0.4. Cargo.lock also lists getrandom 0.3.4
(Cargo.lock:195), so cargo tree -i getrandom will tell you
the name is ambiguous; that entry arrives through secp256k1's
wasm32-only dev-dependency (secp256k1-0.31.1/Cargo.toml:132-134) and is
built for no released target. Exactly one getrandom compiles in:
cargo tree --locked -i getrandom@0.4.3 prints btc-keygen and
nothing else.
With no getrandom_backend cfg set anywhere in the tree,
this is the backend the compiler selects for each of the six targets in
.github/workflows/release.yml:40-62:
| target | backend | call |
|---|---|---|
x86_64-apple-darwin |
getentropy |
getentropy(2) |
aarch64-apple-darwin |
getentropy |
getentropy(2) |
x86_64-unknown-linux-musl |
linux_android_with_fallback |
getrandom(2), else /dev/urandom |
aarch64-unknown-linux-musl |
linux_android_with_fallback |
getrandom(2), else /dev/urandom |
x86_64-unknown-freebsd |
getrandom |
getrandom(2) with flags 0 |
x86_64-pc-windows-gnu |
windows |
ProcessPrng |
The backend column is the checkable one: it names the module
getrandom's own selection chain reaches
(getrandom-0.4.3/src/backends.rs:10-36), so you can read the
cfg_if arms for your target instead of taking the last
column on trust. On macOS the call is getentropy(2) in
256-byte chunks (getrandom-0.4.3/src/backends/getentropy.rs:26-29), with
no retry path and no fallback.
Measured on a local release build for aarch64-apple-darwin, v0.3.0,
rustc 1.97.1, cargo build --release --locked:
nm -u lists 78 undefined symbols, of which exactly one is
randomness-related, _getentropy.
strings -a finds no /dev/urandom,
arc4random, SecRandomCopyBytes,
CCRandomGenerateBytes or rdrand. Treat that
second check as corroboration only: strings does not surface
getentropy on this file either, so a negative result there
cannot tell absent from invisible. nm -u is the check that
carries weight. Two further limits. The import list is a property of one
build of one target, so don't expect that symbol elsewhere. And that file
was built on this machine, not downloaded: the published artefacts are
named btc-keygen-macos-aarch64 and so on
(.github/workflows/release.yml:91-95), and it's the one you actually run
that needs checking, against the release SHA256SUMS.txt.
Two lines in the tree contradicted the table until this was written,
and the released ones still do. A rustdoc comment named
BCryptGenRandom for Windows, which getrandom 0.4.3 does not
call, and docs/04-dependencies.md named getentropy for the
BSDs, which holds for OpenBSD (backends.rs:52-59) and is wrong for
FreeBSD, a released target that takes getrandom(2)
(backends.rs:110-121). Neither line changed what the code does; both were
simply wrong, and both are corrected in the tree. The rustdoc published
for v0.3.0 on docs.rs still shows the old text, because that is what the
released crate contains.
Windows is the one target where the last step is not a kernel entry per
call. Microsoft documents ProcessPrng as the primary
interface to the user-mode per-processor PRNGs, quoted in getrandom's own
source at backends/windows.rs:3-4, and its state is seeded and reseeded by
the kernel, not derived from a serial number, a process ID or a clock.
That description is Microsoft's and was not measured here.
getrandom does check the return value: on emulation layers where
ProcessPrng can fail, Wine through 11.2 and Windows 8, it
returns an error instead of output (backends/windows.rs:50-75), so that
path fails closed too.
The Linux fallback
On the two musl targets there is a fallback, and the filename is not
decoration. The backend probes getrandom(2) once with a
zero-length call (linux_android_with_fallback.rs:35). If the kernel
answers ENOSYS, or on Linux but not Android a seccomp policy answers
EPERM (lines 40, 44-45), it switches for the life of the process to
reading /dev/urandom (use_file.rs:21), and the caller is not
told. Any other errno keeps the syscall. Before the read it opens
/dev/random and polls it with an infinite timeout
(use_file.rs:197-206), which waits on kernel pool initialization. So the
fallback is the kernel's own entropy source, not a generator written
here. It ought to be loud about switching, and it isn't. Note that the
musl builds link getrandom statically
(linux_android_with_fallback.rs:24-30), so the separate
dlsym route into the same fallback cannot fire in them.
That fallback trusts a path. open_readonly
(use_file.rs:55-67) opens /dev/urandom by name and never
fstats the descriptor to confirm it is the kernel character
device, so a container or chroot with a mis-provisioned /dev
can feed the tool whatever file sits at that path. The default syscall
path cannot be substituted that way, which is most of the reason it
matters which path a build takes.
What the poll guarantees is the kernel's own entropy accounting, and
that has changed across versions: on Linux 5.6 and later a readable
/dev/random means the CSPRNG is initialized, whilst older
kernels signalled at an entropy threshold instead, and the ENOSYS branch
fires only on kernels old enough to lack the syscall altogether. This is
reasoned about, not measured. It is also specific to one path:
getentropy(2) on macOS and ProcessPrng on
Windows expose no unseeded state, and nothing in btc-keygen inspects pool
state on any platform. docs/01-threat-model.md:22-24 already records that
a freshly booted air-gapped machine with no hardware RNG may have thin
entropy, and treats that as an operator responsibility. This page does
not retire it.
When the source fails
A source that reports failure fails closed. The error from
fill_bytes propagates with ? at
src/keygen.rs:190 before any validation runs, so a failed draw ends
generation immediately; the retry loop retries only a scalar outside
[1, n-1], and exhausting all 32 attempts returns an error rather than a
key (src/keygen.rs:198-200). src/main.rs:104-107 prints it and returns
ExitCode::FAILURE. Measured: a release build forced onto a
permanently failing backend with
RUSTFLAGS='--cfg getrandom_backend="unsupported"' cargo build
--release --locked wrote zero bytes to stdout, exited 1, and
printed to stderr key generation failed: OS CSPRNG failed:
getrandom: this target is not supported.
A source that reports success with bad bytes is not detected. That is
the honest boundary, and it is the shape the July incident took.
is_valid_key (src/keygen.rs:169) asks libsecp256k1 only
whether the 32 bytes are a scalar in range; it cannot tell random bytes
from predictable ones. There is no entropy health test anywhere in the
tree: no repetition test, no continuous-RNG test, no second source to
compare against. The only stuck source that gets rejected is one stuck at
all zeroes, or at or above the curve order, and only as a side effect of
range validation. One CI test,
test_two_cli_runs_produce_different_keys
(tests/integration.rs:332), fails against a fully stuck source. CI does
not run on your machine, and a source with 32 bits of entropy produces a
different key every run and would pass it.
What a build flag can change
getrandom 0.4 lets the builder replace the backend from the command
line with RUSTFLAGS --cfg getrandom_backend="...", and that
selection happens ahead of platform detection
(getrandom-0.4.3/src/backends.rs:10-36). Measured on
x86_64-apple-darwin:
RUSTFLAGS='--cfg getrandom_backend="rdrand"' cargo build --release
--locked --target x86_64-apple-darwin compiles btc-keygen 0.3.0
with zero warnings, and the resulting binary imports no randomness symbol
at all, where the baseline imports _getentropy. RDRAND
instructions appear in its place. There is no warning because getrandom
declares the cfg and its value set itself, so
unexpected_cfgs never fires on it.
How far that goes: RDRAND and RNDR are hardware CSPRNGs, and both are gated, though not in the same way. RDRAND is gated on CPUID detection plus a one-time eight-draw repeat-value self-test that tolerates up to two repeats and otherwise returns an error rather than degraded output (rdrand.rs:56-68, 115-122, 188-193); the self-test runs once per process, and the fill path (rdrand.rs:124-141) does not re-test. RNDR is gated on FEAT_RNG detection only (rndr.rs:65-113) plus a five-attempt retry when the hardware reports failure (rndr.rs:15, 25); there is no repeat-value test on that path. So a substitution swaps one CSPRNG for another; it does not collapse 128 bits to 32. It is still a downgrade: the key stops depending on the kernel mixing several independent sources and starts depending on one opaque hardware unit, checked once at process start. The one documented RDRAND failure mode, all-ones output, is rejected here, but only as a side effect of range validation.
Of the nine values getrandom 0.4.3 accepts
(getrandom-0.4.3/Cargo.toml:158), only rdrand and
rndr substitute quietly, and neither on every target.
rdrand compiles for the x86_64 targets;
rndr compiles only for aarch64-unknown-linux-musl, and on
aarch64-apple-darwin it is a hard compile error, because getrandom's
std feature is off here and its no_std FEAT_RNG detection is
Linux-only (rndr.rs:106-110). The rest were built and measured on
aarch64-apple-darwin with rustc 1.97.1. custom compiles and
then fails to link: Undefined symbols for architecture arm64:
"___getrandom_v03_custom". That symbol does say v03 inside a 0.4.3
crate; the name is upstream's, not a typo here. Note that
cargo check and cargo clippy pass under
custom, so a check-only CI gate would catch nothing.
extern_impl fails with E0554 on stable and, on nightly,
with #[fill_uninit] function required, but not found, so it
also needs an implementation the builder deliberately supplies.
unsupported builds and then fails closed, as above.
linux_getrandom, linux_raw and
windows_legacy are refused at compile time on every target
where they do not apply, and where they do apply they are the same class
of OS call already in use, down to RtlGenRandom on Windows.
efi_rng cannot be built for any target on the release
matrix, and it is not an OS call in any case: it invokes the UEFI
firmware's EFI_RNG_PROTOCOL (efi_rng.rs:94-117), which runs
before an operating system exists.
A misspelt value is the quiet case, and it happens to be quiet in the
safe direction: it matches no arm of the selection chain, so the platform
default stays in place. "rdrnd", "CUSTOM" (the
values are case-sensitive) and "" all build with no
diagnostic whatsoever, and the binary still imports
_getentropy. Don't wait for a lint to catch a bad value.
rustc's --check-cfg validates cfg(...)
predicates written in source and never inspects --cfg
arguments: measured directly, a made-up value and a made-up cfg name both
compile silently under -D warnings with the full value list
declared. Cargo also builds registry dependencies with
--cap-lints allow, so getrandom's own declaration could not
warn you either.
The override does reach btc-keygen's own compilation unit, not only
getrandom's, which is what makes a guard possible: a temporary
#[cfg(getrandom_backend = "custom")] compile_error!(...) in
src/entropy.rs fired under that flag and stayed silent without it. A
four-line compile_error! rejecting all nine values would
turn every override above into a failed build. The tree now carries one
(src/entropy.rs:1-22): all nine values are rejected, verified by building
under each. No released binary has it. v0.3.0 predates the guard, and a
compile-time check protects whoever compiles the source, not whoever
downloaded a binary from the releases page.
What is not measured
Three gaps, stated outright. The unseeded-kernel behaviour above is
read from the kernel's contract, not observed, because no unseeded kernel
was available to test. That custom also fails to
link on the musl, FreeBSD and Windows targets follows from the same
missing symbol but was measured only on macOS. And the claim that the six
published binaries carry no override rests on there being no
.cargo/config.toml, no occurrence of
getrandom_backend in any tracked file, and no
RUSTFLAGS in release.yml, which is inference about a build
configuration. Inference about build configuration is exactly what failed
in the incident at the top of this page. It is weakest for two targets:
aarch64-unknown-linux-musl and x86_64-unknown-freebsd are built by
cross inside a container image this repository does not
control (.github/workflows/release.yml:79-83). Verify the artefact you
run, not the workflow that made it.
This is not a comparison. btc-keygen is not a hardware wallet: no secure element, no tamper resistance, no supply-chain attestation, no independent audit. It runs on a general-purpose operating system and prints a private key to a terminal. A dedicated device remains the stronger choice for most people, incident or no incident. What this page documents is one property, where the 32 bytes come from, and one property is not a threat model. The threat model is at docs/01-threat-model.md.
Important. Nothing in any released binary asserts, at build time or run time, which entropy backend it was compiled with. The guard in the tree fails a build, which is no help to a binary already built. The reason to believe any of the above is that every line of it is reproducible from the commands named. btc-keygen is alpha. Not independently audited. Do not use with funds you cannot afford to lose.
Security design
This tool was designed as if every run could protect someone's life savings. Here is what is under the hood, in plain language:
- Randomness from your OS
- Your private key is generated using your operating system's cryptographic random number generator, the same source used by SSH, TLS, and disk encryption.
- Bitcoin Core's own math library
- The elliptic curve operations use libsecp256k1, the same code that powers Bitcoin Core. The most reviewed implementation in the Bitcoin ecosystem.
- Secrets are erased from memory
- Every buffer the tool owns is overwritten with zeros as soon as it is finished with it, so keys are not left sitting in RAM. Three copies are beyond its reach: what your terminal keeps on screen, anything the operating system paged out to swap or wrote to a crash dump, and copies the compiler or the maths library may leave on the stack. Disable swap and core dumps if that matters for your threat model.
- Zero network code
- The tool contains no networking code at all. It cannot connect to the internet, phone home, or leak your keys over any channel.
- Minimal and auditable
- The entire codebase is under 500 lines of Rust with 6 dependencies. A competent developer can read and verify the whole program in an afternoon.
- Tested against known vectors
- Every component is tested against published Bitcoin test vectors, ensuring the keys and addresses are valid on the real Bitcoin network.
For the full technical threat model, security assumptions, and dependency analysis, see the design documentation in the repository.
Download
Pre-built binaries are available for every major platform. Each binary is a single file with no dependencies: download it, verify the checksum, and run it.
Always verify the checksum. Download the
SHA256SUMS.txt file from the release and compare it against
the binary you downloaded. This ensures the file has not been tampered
with.
Linux (x86_64)
curl -LO https://github.com/aguimaraes/btc-keygen/releases/latest/download/btc-keygen-linux-x86_64
sha256sum btc-keygen-linux-x86_64
chmod +x btc-keygen-linux-x86_64
./btc-keygen-linux-x86_64 generate
Linux (ARM64 / Raspberry Pi)
curl -LO https://github.com/aguimaraes/btc-keygen/releases/latest/download/btc-keygen-linux-aarch64
sha256sum btc-keygen-linux-aarch64
chmod +x btc-keygen-linux-aarch64
./btc-keygen-linux-aarch64 generate
macOS (Apple Silicon)
curl -LO https://github.com/aguimaraes/btc-keygen/releases/latest/download/btc-keygen-macos-aarch64
shasum -a 256 btc-keygen-macos-aarch64
chmod +x btc-keygen-macos-aarch64
./btc-keygen-macos-aarch64 generate
macOS may show a Gatekeeper warning. To bypass:
xattr -d com.apple.quarantine btc-keygen-macos-aarch64.
macOS (Intel)
curl -LO https://github.com/aguimaraes/btc-keygen/releases/latest/download/btc-keygen-macos-x86_64
shasum -a 256 btc-keygen-macos-x86_64
chmod +x btc-keygen-macos-x86_64
./btc-keygen-macos-x86_64 generate
Windows
Get-FileHash btc-keygen-windows-x86_64.exe -Algorithm SHA256
.\btc-keygen-windows-x86_64.exe generate
Download btc-keygen-windows-x86_64.exe from the release
page.
Build from source
Requires Rust and a C compiler (gcc, clang, or MSVC).
git clone https://github.com/aguimaraes/btc-keygen.git
cd btc-keygen
cargo build --release
./target/release/btc-keygen generate
BSD
Pre-built binaries are not provided for BSD. Build from source using the instructions above. The tool compiles and runs on FreeBSD, OpenBSD, and NetBSD.
Usage
Generate a keypair
$ btc-keygen generate
address: bc1q...
wif: K...
Prints the Bitcoin address and the WIF private key. That is all you need to receive and later spend Bitcoin.
Include the raw private key in hex
$ btc-keygen generate --hex
address: bc1q...
wif: K...
private_key_hex: ab12cd...
Include the compressed public key
$ btc-keygen generate --pubkey
address: bc1q...
wif: K...
pubkey_hex: 02ab12cd...
JSON output (for scripts)
$ btc-keygen generate --json
{"address":"bc1q...","wif":"K..."}
All options combined
$ btc-keygen generate --hex --pubkey --json
Provide your own private key
If you have your own 32 bytes of key material (for example, from physical dice rolls converted to hex), you can skip OS entropy and use your bytes directly. The tool validates that the value is a valid secp256k1 scalar, then derives the WIF, public key, and address from it.
$ btc-keygen generate --from-hex 0000000000000000000000000000000000000000000000000000000000000001
address: bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4
wif: KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn
With --from-hex, the security of the generated key depends
entirely on how you sourced those 32 bytes. The OS random number generator
is not used.
What each field means
- address
- Your Bitcoin receiving address. Share it with anyone who wants to
send you Bitcoin. Starts with
bc1q(native SegWit). - wif
- Wallet Import Format: your private key encoded in a standard format
that any Bitcoin wallet can read. Starts with
KorL. Keep it secret. See Using your keys for how to use it. - private_key_hex
- The raw 32-byte private key in hexadecimal. Same key as the WIF, different format. Optional.
- pubkey_hex
- The compressed public key derived from the private key. Not secret, but not needed for basic use. Optional.
Frequently asked questions
Can I run this tool again to get the same key?
No. Every run generates a completely new, random keypair. There is no way to recreate a previous key. Write it down the first time.
What if I lose my private key?
Any Bitcoin sent to that address is permanently inaccessible. There is no recovery mechanism. This is by design: if anyone could recover your key, so could an attacker.
Why not use a hardware wallet?
Hardware wallets are a good option. This tool is an alternative for people who prefer not to trust a hardware device manufactured by a third party, or who want a simple, verifiable way to generate a single key for long-term storage.
Why not use a BIP39 mnemonic (seed words)?
Mnemonic phrases are useful for HD wallets that generate many addresses. This tool intentionally generates a single standalone key. It's simpler, easier to audit, and sufficient for cold storage of a single address. BIP39 support may be considered in a future version.
Is this a wallet?
No. This is a key generator. It creates a keypair and exits. It cannot send Bitcoin, check balances, or interact with the Bitcoin network in any way. To spend Bitcoin, you'll need to import the private key into a wallet application. See Using your address and private key for step-by-step instructions.
For developers
btc-keygen is also available as a Rust library. If you are building software that needs to generate Bitcoin keys, you can use it as a dependency instead of shelling out to the binary.
Add to your project
cargo add btc-keygen
API
The library exposes four functions and one type:
| function | input | output |
|---|---|---|
generate() |
(none) | Result<PrivateKey, Error> |
encode_wif(&key) |
&PrivateKey |
String starts with K/L |
derive_pubkey(&key) |
&PrivateKey |
[u8; 33] compressed pubkey |
derive_address(&pubkey) |
&[u8; 33] |
String Bech32 (bc1q…) |
Example
use btc_keygen;
fn main() -> Result<(), btc_keygen::Error> {
// 1. Generate a private key from OS randomness
let key = btc_keygen::generate()?;
// 2. Encode as WIF (for wallet import)
let wif = btc_keygen::encode_wif(&key);
// 3. Derive the compressed public key
let pubkey = btc_keygen::derive_pubkey(&key);
// 4. Derive the Bitcoin address
let address = btc_keygen::derive_address(&pubkey);
println!("Address: {address}");
println!("WIF: {}", wif.expose_str());
Ok(())
}
PrivateKey zeroizes its bytes when dropped, and
encode_wif returns a SecretWif rather than a
String, so the WIF erases itself too. Neither can be printed with
{} by accident; reading one is spelled
expose_str(), and whatever you do with the result afterwards is
outside what the library can erase.
Full API documentation is available on docs.rs.