flake.nix and declarative command line interface

This commit is contained in:
janis 2025-09-03 15:35:40 +02:00
parent 0d242d16d7
commit aca589397e
Signed by: janis
SSH key fingerprint: SHA256:bB1qbbqmDXZNT0KKD5c2Dfjg53JGhj7B3CFcLIzSqq8
9 changed files with 1651 additions and 20 deletions

1
.envrc Normal file
View file

@ -0,0 +1 @@
use flake

3
.gitignore vendored
View file

@ -1,2 +1,3 @@
/target
Cargo.lock
/result
/.direnv

1301
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[package]
name = "duralumin"
version = "0.3.0"
version = "0.4.0"
edition = "2021"
[lib]
@ -25,7 +25,7 @@ required-features = ["ed25519", "clap", "rpassword", "base64"]
[dependencies]
rand = "0.8"
clap = {version = "3.0.0-beta.5", optional = true, features = ["derive"]}
clap = {version = "4.5", optional = true, features = ["derive", "cargo"]}
base64 = {version = "0.13", optional = true}
bytes = {version = "1.1", optional = true}
sha2 = {version = "0.9", optional = true}

96
flake.lock Normal file
View file

@ -0,0 +1,96 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1756787288,
"narHash": "sha256-rw/PHa1cqiePdBxhF66V7R+WAP8WekQ0mCDG4CFqT8Y=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "d0fc30899600b9b3466ddb260fd83deb486c32f1",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1744536153,
"narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"rust-overlays": "rust-overlays"
}
},
"rust-overlays": {
"inputs": {
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1756866691,
"narHash": "sha256-YWJsM0HfdFLcaoP5OeyzjX6MjGnJ0Acm+bg1QN8MKjo=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "fb6dab6f320291a8edd31c1d67f078c6f7384a02",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

49
flake.nix Normal file
View file

@ -0,0 +1,49 @@
{
description = "A tool for deterministically generating SSH keys";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
rust-overlays.url = "github:oxalica/rust-overlay";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils, rust-overlays, ...}:
flake-utils.lib.eachDefaultSystem (system: let
pkgs = import nixpkgs {
inherit system;
overlays = [
(import rust-overlays)
self.overlays.default
];
};
rust = pkgs.rust-bin.stable.latest.default;
in {
devShells.default =
pkgs.mkShell {
buildInputs = with pkgs; [
pkg-config
git
rust
];
};
packages = rec {
inherit (pkgs) duralumin duralumin-keygen;
default = duralumin;
};
}) // {
overlays.default = final: prev: let
toolchain = final.rust-bin.stable.latest.default;
rustPlatform = final.makeRustPlatform {
cargo = toolchain;
rustc = toolchain;
};
in {
duralumin = prev.callPackage ./pkgs/package.nix { inherit rustPlatform; };
duralumin-keygen = prev.callPackage ./pkgs/keygen.nix {
inherit (final) duralumin;
};
};
};
}

26
pkgs/keygen.nix Normal file
View file

@ -0,0 +1,26 @@
{lib, stdenv, duralumin, ...}: stdenv.mkDerivation {
pname = "duralumin-keygen";
version = "0.4.0";
dontConfigure = true;
dontBuild = true;
dontUnpack = true;
outputs = [ "out" ];
installPhase = ''
runHook preInstall
mkdir -p $out/bin
ln -s ${duralumin}/bin/duralumin-keygen $out/bin/
runHook postInstall
'';
meta = with lib; {
description = "A tool for deterministically generating SSH keys";
homepage = "https://git.nirgendwo.xyz/janis/duralumin";
license = licenses.mit;
maintainers = with maintainers; [ janis ];
mainProgram = "duralumin";
};
}

26
pkgs/package.nix Normal file
View file

@ -0,0 +1,26 @@
{lib, rustPlatform, pkg-config, ...}:
rustPlatform.buildRustPackage {
pname = "duralumin";
version = "0.4.0";
src = ../.;
cargoLock = {
lockFile = ../Cargo.lock;
};
cargoSha256 = "sha256-+X1mXG7rXUu0Yk3jv2b0mYH2mXKX9I6w5Fqz1n8y4mE=";
nativeBuildInputs = [ pkg-config ];
buildInputs = [ ];
meta = with lib; {
description = "A tool for generating keyphrases and passwords";
homepage = "https://git.nirgendwo.xyz/janis/duralumin";
license = licenses.mit;
maintainers = with maintainers; [ janis ];
mainProgram = "duralumin";
};
}

View file

@ -1,29 +1,160 @@
use clap::Parser;
use std::path::PathBuf;
/// program that generates ed25519 keypairs seeded by a passphrase and an optional ID.
#[derive(Parser)]
#[clap(
name = "duralumin-keygen",
version = "0.3.0",
author = "No One <noonebtw@nirgendwo.xyz>"
)]
struct Opts {
#[clap(short, long)]
file: Option<String>,
use clap::{command, value_parser, ArgAction, Command, Parser};
use libduralumin::key_gen::{HashDesc, KeygenDesc};
use zeroize::Zeroizing;
fn args() -> clap::ArgMatches {
let sha256 = Command::new("sha256")
.about("Use sha256 as the hash function")
.arg(
clap::arg!(-i --iterations <ITERATIONS> "Number of iterations")
.required(true)
.value_parser(value_parser!(u32))
.default_value("4"),
);
let argon2 = Command::new("argon2")
.about("Use argon2 as the hash function")
.arg_required_else_help(true)
.arg(
clap::arg!(-v --variant <VARIANT> "Variant of argon2 to use")
.required(false)
.value_parser(["argon2d", "argon2i", "argon2id"])
.default_value("argon2id"),
)
.arg(
clap::arg!(-m --memory <MEMORY> "Memory cost in mibibytes (MiB)")
.required(false)
.value_parser(value_parser!(u32))
.default_value("64"),
)
.arg(
clap::arg!(-t --time <TIME> "Time cost (number of iterations)")
.required(false)
.value_parser(value_parser!(u32))
.default_value("4"),
)
.arg(
clap::arg!(-p --parallelism <PARALLELISM> "Degree of parallelism (lanes)")
.required(false)
.value_parser(value_parser!(u32))
.default_value("1"),
);
let declare = Command::new("declare")
.about("Declare key definition via the command line instead of interactively")
.arg(
clap::arg!(-t --tag <TAG> "Tag to identify the keypair")
.required(false)
.value_parser(value_parser!(String)),
)
.arg(
clap::arg!(--encrypt "Whether to encrypt the private key on disk")
.required(false)
.action(ArgAction::SetTrue),
)
.arg(
clap::arg!(--passphrase <PASSPHRASE> "Passphrase to seed the keypair generation")
.required(true)
.value_parser(value_parser!(String)),
)
.subcommands([sha256, argon2]);
let interactive = Command::new("interactive").about("Interactively prompt for key definition");
let matches = command!()
.about("Duralumin SSH Keypair Generator")
.long_about("A tool to deterministically generate ed25519 SSH keypairs with secure passphrase-based encryption and hashing options.")
.author("Janis <janis@nirgendwo.xyz>")
.arg(
clap::arg!(-f --file <FILE> "Path to save the keypair to.")
.required(false)
.value_parser(value_parser!(PathBuf)),
)
.subcommands([declare, interactive])
.get_matches();
matches
}
fn desc_from_args(args: &clap::ArgMatches) -> KeygenDesc {
match args.subcommand() {
Some(("declare", matches)) => {
let tag = matches
.get_one::<String>("tag")
.cloned()
.map(Zeroizing::new);
let passphrase = matches
.get_one::<String>("passphrase")
.cloned()
.map(Zeroizing::new)
.expect("Passphrase is required");
let encrypt = matches.get_flag("encrypt");
match matches.subcommand() {
Some(("sha256", matches)) => {
let iterations = matches.get_one::<u32>("iterations").copied().unwrap_or(4);
KeygenDesc {
hash: libduralumin::key_gen::HashDesc::Sha256 { iterations },
salt: KeygenDesc::SALT.to_vec(),
tag,
passphrase,
encrypt,
}
}
Some(("argon2", matches)) => {
let variant = matches
.get_one::<String>("variant")
.map(String::as_str)
.unwrap_or("argon2id");
let memory = matches.get_one::<u32>("memory").copied().unwrap_or(64) * 1024;
let time = matches.get_one::<u32>("time").copied().unwrap_or(4);
let lanes = matches.get_one::<u32>("parallelism").copied().unwrap_or(1);
let variant = match variant {
"argon2d" => argon2::Algorithm::Argon2d,
"argon2i" => argon2::Algorithm::Argon2i,
"argon2id" => argon2::Algorithm::Argon2id,
_ => unreachable!("clap should ensure this"),
};
let hash = HashDesc::Argon {
variant,
memory,
time,
lanes,
version: argon2::Version::V0x13,
hash_length: 32,
};
KeygenDesc {
hash,
salt: KeygenDesc::SALT.to_vec(),
tag,
passphrase,
encrypt,
}
}
_ => panic!("A subcommand is required"),
}
}
Some(("interactive", _)) => libduralumin::key_gen::cli::keygen_desc_from_stdin()
.expect("Failed to read keygen description from stdin"),
_ => panic!("A subcommand is required"),
}
}
fn main() -> anyhow::Result<()> {
let opts = Opts::parse();
let args = args();
println!("Generating ed25519 ssh keypair:");
let desc = libduralumin::key_gen::cli::keygen_desc_from_stdin()?;
let desc = desc_from_args(&args);
let base_path = opts.file.unwrap_or_else(|| {
if let Some(tag) = desc.tag.as_ref() {
let file = args.get_one::<PathBuf>("file").cloned();
let base_path = file.unwrap_or_else(|| {
PathBuf::from(if let Some(tag) = desc.tag.as_ref() {
format!("duralumin_{}", tag.as_str())
} else {
"duralumin".to_owned()
}
})
});
let keypair = libduralumin::key_gen::generate_key(desc)?;
@ -39,7 +170,7 @@ fn main() -> anyhow::Result<()> {
);
let private_path = base_path.clone();
let public_path = base_path.clone() + ".pub";
let public_path = base_path.with_extension("pub");
let (private_key, public_key) = keypair.encode_keys()?;
std::fs::write(&private_path, private_key)?;