flake.nix and declarative command line interface
This commit is contained in:
parent
0d242d16d7
commit
aca589397e
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
||||||
/target
|
/target
|
||||||
Cargo.lock
|
/result
|
||||||
|
/.direnv
|
||||||
|
|
1301
Cargo.lock
generated
Normal file
1301
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "duralumin"
|
name = "duralumin"
|
||||||
version = "0.3.0"
|
version = "0.4.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
|
@ -25,7 +25,7 @@ required-features = ["ed25519", "clap", "rpassword", "base64"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rand = "0.8"
|
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}
|
base64 = {version = "0.13", optional = true}
|
||||||
bytes = {version = "1.1", optional = true}
|
bytes = {version = "1.1", optional = true}
|
||||||
sha2 = {version = "0.9", optional = true}
|
sha2 = {version = "0.9", optional = true}
|
||||||
|
|
96
flake.lock
Normal file
96
flake.lock
Normal 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
49
flake.nix
Normal 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
26
pkgs/keygen.nix
Normal 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
26
pkgs/package.nix
Normal 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";
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
|
@ -1,29 +1,160 @@
|
||||||
use clap::Parser;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
/// program that generates ed25519 keypairs seeded by a passphrase and an optional ID.
|
use clap::{command, value_parser, ArgAction, Command, Parser};
|
||||||
#[derive(Parser)]
|
use libduralumin::key_gen::{HashDesc, KeygenDesc};
|
||||||
#[clap(
|
use zeroize::Zeroizing;
|
||||||
name = "duralumin-keygen",
|
|
||||||
version = "0.3.0",
|
fn args() -> clap::ArgMatches {
|
||||||
author = "No One <noonebtw@nirgendwo.xyz>"
|
let sha256 = Command::new("sha256")
|
||||||
)]
|
.about("Use sha256 as the hash function")
|
||||||
struct Opts {
|
.arg(
|
||||||
#[clap(short, long)]
|
clap::arg!(-i --iterations <ITERATIONS> "Number of iterations")
|
||||||
file: Option<String>,
|
.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<()> {
|
fn main() -> anyhow::Result<()> {
|
||||||
let opts = Opts::parse();
|
let args = args();
|
||||||
println!("Generating ed25519 ssh keypair:");
|
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(|| {
|
let file = args.get_one::<PathBuf>("file").cloned();
|
||||||
if let Some(tag) = desc.tag.as_ref() {
|
let base_path = file.unwrap_or_else(|| {
|
||||||
|
PathBuf::from(if let Some(tag) = desc.tag.as_ref() {
|
||||||
format!("duralumin_{}", tag.as_str())
|
format!("duralumin_{}", tag.as_str())
|
||||||
} else {
|
} else {
|
||||||
"duralumin".to_owned()
|
"duralumin".to_owned()
|
||||||
}
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
let keypair = libduralumin::key_gen::generate_key(desc)?;
|
let keypair = libduralumin::key_gen::generate_key(desc)?;
|
||||||
|
@ -39,7 +170,7 @@ fn main() -> anyhow::Result<()> {
|
||||||
);
|
);
|
||||||
|
|
||||||
let private_path = base_path.clone();
|
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()?;
|
let (private_key, public_key) = keypair.encode_keys()?;
|
||||||
std::fs::write(&private_path, private_key)?;
|
std::fs::write(&private_path, private_key)?;
|
||||||
|
|
Loading…
Reference in a new issue