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
|
||||
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]
|
||||
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
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.
|
||||
#[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)?;
|
||||
|
|
Loading…
Reference in a new issue