Tidy up environment variables

* Try to use constants instead of strings
* Move crypto seed override out of radicle-crypto
* Use GIT_COMMITTER_DATE for commit overrides
* Use RAD_LOCAL_TIME as general time override
* Allow variables to be used in release mode
This commit is contained in:
cloudhead 2024-04-23 17:36:23 +02:00
parent deeb39c558
commit fe5757d122
No known key found for this signature in database
18 changed files with 154 additions and 126 deletions

View File

@ -1,5 +1,4 @@
#![allow(clippy::or_fun_call)] #![allow(clippy::or_fun_call)]
use std::env;
use std::ffi::OsString; use std::ffi::OsString;
use std::ops::Not as _; use std::ops::Not as _;
use std::str::FromStr; use std::str::FromStr;
@ -9,7 +8,7 @@ use anyhow::anyhow;
use radicle::crypto::ssh; use radicle::crypto::ssh;
use radicle::crypto::ssh::Passphrase; use radicle::crypto::ssh::Passphrase;
use radicle::node::Alias; use radicle::node::Alias;
use radicle::profile::env::RAD_PASSPHRASE; use radicle::profile::env;
use radicle::{profile, Profile}; use radicle::{profile, Profile};
use crate::terminal as term; use crate::terminal as term;
@ -108,11 +107,11 @@ pub fn init(options: Options) -> anyhow::Result<()> {
let passphrase = if options.stdin { let passphrase = if options.stdin {
term::passphrase_stdin() term::passphrase_stdin()
} else { } else {
term::passphrase_confirm("Enter a passphrase:", RAD_PASSPHRASE) term::passphrase_confirm("Enter a passphrase:", env::RAD_PASSPHRASE)
}?; }?;
let passphrase = passphrase.trim().is_empty().not().then_some(passphrase); let passphrase = passphrase.trim().is_empty().not().then_some(passphrase);
let spinner = term::spinner("Creating your Ed25519 keypair..."); let spinner = term::spinner("Creating your Ed25519 keypair...");
let profile = Profile::init(home, alias, passphrase.clone())?; let profile = Profile::init(home, alias, passphrase.clone(), env::seed())?;
let mut agent = true; let mut agent = true;
spinner.finish(); spinner.finish();
@ -199,7 +198,7 @@ pub fn authenticate(options: Options, profile: &Profile) -> anyhow::Result<()> {
// Try RAD_PASSPHRASE fallback. // Try RAD_PASSPHRASE fallback.
if let Some(passphrase) = profile::env::passphrase() { if let Some(passphrase) = profile::env::passphrase() {
ssh::keystore::MemorySigner::load(&profile.keystore, Some(passphrase)) ssh::keystore::MemorySigner::load(&profile.keystore, Some(passphrase))
.map_err(|_| anyhow!("`{RAD_PASSPHRASE}` is invalid"))?; .map_err(|_| anyhow!("`{}` is invalid", env::RAD_PASSPHRASE))?;
return Ok(()); return Ok(());
} }

View File

@ -5,12 +5,12 @@ use localtime::LocalTime;
pub use radicle_term::format::*; pub use radicle_term::format::*;
pub use radicle_term::{style, Paint}; pub use radicle_term::{style, Paint};
use radicle::cob::{ObjectId, Timestamp}; use radicle::cob::ObjectId;
use radicle::identity::Visibility; use radicle::identity::Visibility;
use radicle::node::policy::Policy; use radicle::node::policy::Policy;
use radicle::node::{Alias, AliasStore, NodeId}; use radicle::node::{Alias, AliasStore, NodeId};
use radicle::prelude::Did; use radicle::prelude::Did;
use radicle::profile::Profile; use radicle::profile::{env, Profile};
use radicle::storage::RefUpdate; use radicle::storage::RefUpdate;
use radicle_term::element::Line; use radicle_term::element::Line;
@ -74,8 +74,8 @@ pub fn policy(p: &Policy) -> Paint<String> {
/// Format a timestamp. /// Format a timestamp.
pub fn timestamp(time: impl Into<LocalTime>) -> Paint<String> { pub fn timestamp(time: impl Into<LocalTime>) -> Paint<String> {
let time: LocalTime = time.into(); let time = time.into();
let now: LocalTime = Timestamp::now().into(); let now = env::local_time();
let duration = now - time; let duration = now - time;
let fmt = timeago::Formatter::new(); let fmt = timeago::Formatter::new();

View File

@ -1,6 +1,6 @@
use std::path::Path; use std::path::Path;
use std::str::FromStr; use std::str::FromStr;
use std::{env, net, thread, time}; use std::{net, thread, time};
use radicle::git; use radicle::git;
use radicle::node; use radicle::node;
@ -11,7 +11,7 @@ use radicle::node::Handle as _;
use radicle::node::{Address, Alias, DEFAULT_TIMEOUT}; use radicle::node::{Address, Alias, DEFAULT_TIMEOUT};
use radicle::prelude::{NodeId, RepoId}; use radicle::prelude::{NodeId, RepoId};
use radicle::profile; use radicle::profile;
use radicle::profile::Home; use radicle::profile::{env, Home};
use radicle::storage::{ReadStorage, RefUpdate, RemoteRepository}; use radicle::storage::{ReadStorage, RefUpdate, RemoteRepository};
use radicle::test::fixtures; use radicle::test::fixtures;
@ -105,14 +105,14 @@ fn formula(root: &Path, test: impl AsRef<Path>) -> Result<TestFormula, Box<dyn s
.env("GIT_COMMITTER_DATE", "1671125284") .env("GIT_COMMITTER_DATE", "1671125284")
.env("GIT_COMMITTER_EMAIL", "radicle@localhost") .env("GIT_COMMITTER_EMAIL", "radicle@localhost")
.env("GIT_COMMITTER_NAME", "radicle") .env("GIT_COMMITTER_NAME", "radicle")
.env("RAD_PASSPHRASE", "radicle")
.env("RAD_SEED", RAD_SEED)
.env("RAD_RNG_SEED", "0")
.env("EDITOR", "true") .env("EDITOR", "true")
.env("TZ", "UTC") .env("TZ", "UTC")
.env("LANG", "C") .env("LANG", "C")
.env("USER", "alice") .env("USER", "alice")
.env(radicle_cob::git::RAD_COMMIT_TIME, "1671125284") .env(env::RAD_PASSPHRASE, "radicle")
.env(env::RAD_SEED, RAD_SEED)
.env(env::RAD_RNG_SEED, "0")
.env(env::RAD_LOCAL_TIME, "1671125284")
.envs(git::env::GIT_DEFAULT_CONFIG) .envs(git::env::GIT_DEFAULT_CONFIG)
.build(&[ .build(&[
("radicle-remote-helper", "git-remote-rad"), ("radicle-remote-helper", "git-remote-rad"),

View File

@ -1,8 +1,8 @@
// Copyright © 2022 The Radicle Link Contributors // Copyright © 2022 The Radicle Team
pub mod change; pub mod change;
/// Environment variable to set to overwrite the commit date for both the author and the committer. /// Environment variable to set to overwrite the commit date for both the author and the committer.
/// ///
/// The format must be a unix timestamp. /// The format must be a unix timestamp.
pub const RAD_COMMIT_TIME: &str = "RAD_COMMIT_TIME"; pub const GIT_COMMITTER_DATE: &str = "GIT_COMMITTER_DATE";

View File

@ -308,11 +308,13 @@ fn write_commit(
}, },
1514817556, 1514817556,
); );
#[cfg(debug_assertions)] let (author, timestamp) = if let Ok(s) = std::env::var(crate::git::GIT_COMMITTER_DATE) {
let (author, timestamp) = if let Ok(s) = std::env::var(crate::git::RAD_COMMIT_TIME) { let Ok(timestamp) = s.trim().parse::<i64>() else {
// SAFETY: It's ok to panic here, since this is only enabled in debug mode. panic!(
#[allow(clippy::unwrap_used)] "Invalid timestamp value {s:?} for `{}`",
let timestamp = s.trim().parse::<i64>().unwrap(); crate::git::GIT_COMMITTER_DATE
);
};
let author = Author { let author = Author {
time: git_ext::author::Time::new(timestamp, 0), time: git_ext::author::Time::new(timestamp, 0),
..author ..author

View File

@ -485,43 +485,6 @@ impl sqlite::BindableWithIndex for &Signature {
} }
} }
pub mod keypair {
use super::*;
/// Generate a new keypair using OS randomness.
pub fn generate() -> KeyPair {
#[cfg(debug_assertions)]
if let Some(seed) = env::seed() {
// Generate a keypair based on the given environment variable.
// This is useful for debugging and testing, since the
// public key can be known in advance.
return KeyPair::from_seed(Seed::new(seed));
}
KeyPair::generate()
}
}
pub mod env {
use std::env;
/// Return the seed stored in the `RAD_SEED` environment variable, if any.
pub fn seed() -> Option<[u8; 32]> {
if let Ok(seed) = env::var("RAD_SEED") {
let seed = (0..seed.len())
.step_by(2)
.map(|i| u8::from_str_radix(&seed[i..i + 2], 16))
.collect::<Result<Vec<u8>, _>>()
.expect("env::seed: invalid hexadecimal value set in `RAD_SEED`");
let seed: [u8; 32] = seed
.try_into()
.expect("env::seed: invalid seed length set in `RAD_SEED`");
return Some(seed);
}
None
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::KeyPair; use super::KeyPair;

View File

@ -8,7 +8,7 @@ use cyphernet::{EcSk, EcSkInvalid, Ecdh};
use thiserror::Error; use thiserror::Error;
use zeroize::Zeroizing; use zeroize::Zeroizing;
use crate::{keypair, KeyPair, PublicKey, SecretKey, Signature, Signer, SignerError}; use crate::{KeyPair, PublicKey, SecretKey, Signature, Signer, SignerError};
/// A secret key passphrase. /// A secret key passphrase.
pub type Passphrase = Zeroizing<String>; pub type Passphrase = Zeroizing<String>;
@ -58,10 +58,16 @@ impl Keystore {
/// ///
/// The `comment` is associated with the private key. /// The `comment` is associated with the private key.
/// The `passphrase` is used to encrypt the private key. /// The `passphrase` is used to encrypt the private key.
/// The `seed` is used to derive the private key and should almost always be generated.
/// ///
/// If `passphrase` is `None`, the key is not encrypted. /// If `passphrase` is `None`, the key is not encrypted.
pub fn init(&self, comment: &str, passphrase: Option<Passphrase>) -> Result<PublicKey, Error> { pub fn init(
self.store(keypair::generate(), comment, passphrase) &self,
comment: &str,
passphrase: Option<Passphrase>,
seed: ec25519::Seed,
) -> Result<PublicKey, Error> {
self.store(KeyPair::from_seed(seed), comment, passphrase)
} }
/// Store a keypair on disk. Returns an error if the key already exists. /// Store a keypair on disk. Returns an error if the key already exists.
@ -285,7 +291,11 @@ mod tests {
let store = Keystore::new(&tmp.path()); let store = Keystore::new(&tmp.path());
let public = store let public = store
.init("test", Some("hunter".to_owned().into())) .init(
"test",
Some("hunter".to_owned().into()),
ec25519::Seed::default(),
)
.unwrap(); .unwrap();
assert_eq!(public, store.public_key().unwrap().unwrap()); assert_eq!(public, store.public_key().unwrap().unwrap());
assert!(store.is_encrypted().unwrap()); assert!(store.is_encrypted().unwrap());
@ -306,7 +316,7 @@ mod tests {
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
let store = Keystore::new(&tmp.path()); let store = Keystore::new(&tmp.path());
let public = store.init("test", None).unwrap(); let public = store.init("test", None, ec25519::Seed::default()).unwrap();
assert_eq!(public, store.public_key().unwrap().unwrap()); assert_eq!(public, store.public_key().unwrap().unwrap());
assert!(!store.is_encrypted().unwrap()); assert!(!store.is_encrypted().unwrap());
@ -320,7 +330,11 @@ mod tests {
let store = Keystore::new(&tmp.path()); let store = Keystore::new(&tmp.path());
let public = store let public = store
.init("test", Some("hunter".to_owned().into())) .init(
"test",
Some("hunter".to_owned().into()),
ec25519::Seed::default(),
)
.unwrap(); .unwrap();
let signer = MemorySigner::load(&store, Some("hunter".to_owned().into())).unwrap(); let signer = MemorySigner::load(&store, Some("hunter".to_owned().into())).unwrap();

View File

@ -1,8 +1,8 @@
use std::collections::BTreeSet; use std::collections::BTreeSet;
use std::fs;
use std::path::Path; use std::path::Path;
use std::str::FromStr; use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
use std::{env, fs};
use axum::body::{Body, Bytes}; use axum::body::{Body, Bytes};
use axum::http::{Method, Request}; use axum::http::{Method, Request};
@ -17,7 +17,7 @@ use radicle::crypto::ssh::Keystore;
use radicle::crypto::{KeyPair, Seed, Signer}; use radicle::crypto::{KeyPair, Seed, Signer};
use radicle::git::{raw as git2, RefString}; use radicle::git::{raw as git2, RefString};
use radicle::identity::Visibility; use radicle::identity::Visibility;
use radicle::profile::Home; use radicle::profile::{env, Home};
use radicle::storage::ReadStorage; use radicle::storage::ReadStorage;
use radicle::Storage; use radicle::Storage;
use radicle::{node, profile}; use radicle::{node, profile};
@ -144,7 +144,7 @@ fn seed_with_signer<G: Signer>(dir: &Path, profile: radicle::Profile, signer: &G
let workdir = dir.join("hello-world"); let workdir = dir.join("hello-world");
env::set_var("RAD_COMMIT_TIME", TIMESTAMP.to_string()); env::set_var(env::GIT_COMMITTER_DATE, TIMESTAMP.to_string());
fs::create_dir_all(&workdir).unwrap(); fs::create_dir_all(&workdir).unwrap();

View File

@ -3,7 +3,7 @@ use std::mem::ManuallyDrop;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::{ use std::{
collections::{BTreeMap, BTreeSet}, collections::{BTreeMap, BTreeSet},
env, fs, io, iter, net, process, thread, time, fs, io, iter, net, process, thread, time,
time::Duration, time::Duration,
}; };
@ -24,7 +24,7 @@ use radicle::node::Database;
use radicle::node::{Alias, POLICIES_DB_FILE}; use radicle::node::{Alias, POLICIES_DB_FILE};
use radicle::node::{ConnectOptions, Handle as _}; use radicle::node::{ConnectOptions, Handle as _};
use radicle::profile; use radicle::profile;
use radicle::profile::{Home, Profile}; use radicle::profile::{env, Home, Profile};
use radicle::rad; use radicle::rad;
use radicle::storage::{ReadStorage as _, RemoteRepository as _, SignRepository as _}; use radicle::storage::{ReadStorage as _, RemoteRepository as _, SignRepository as _};
use radicle::test::fixtures; use radicle::test::fixtures;
@ -413,11 +413,14 @@ impl<G: Signer + cyphernet::Ecdh> NodeHandle<G> {
.env("GIT_COMMITTER_DATE", "1671125284") .env("GIT_COMMITTER_DATE", "1671125284")
.env("GIT_COMMITTER_EMAIL", "radicle@localhost") .env("GIT_COMMITTER_EMAIL", "radicle@localhost")
.env("GIT_COMMITTER_NAME", "radicle") .env("GIT_COMMITTER_NAME", "radicle")
.env("RAD_HOME", self.home.path().to_string_lossy().to_string()) .env(
.env("RAD_PASSPHRASE", "radicle") env::RAD_HOME,
self.home.path().to_string_lossy().to_string(),
)
.env(env::RAD_PASSPHRASE, "radicle")
.env(env::RAD_LOCAL_TIME, "1671125284")
.env("TZ", "UTC") .env("TZ", "UTC")
.env("LANG", "C") .env("LANG", "C")
.env(radicle::cob::git::RAD_COMMIT_TIME, "1671125284")
.envs(git::env::GIT_DEFAULT_CONFIG) .envs(git::env::GIT_DEFAULT_CONFIG)
.current_dir(cwd) .current_dir(cwd)
.arg(cmd) .arg(cmd)

View File

@ -120,7 +120,7 @@ pub fn run(profile: radicle::Profile) -> Result<(), Error> {
.map(PathBuf::from) .map(PathBuf::from)
.map_err(|_| Error::NoGitDir)?; .map_err(|_| Error::NoGitDir)?;
// Whether we should output debug logs. // Whether we should output debug logs.
let debug = env::var("RAD_DEBUG").is_ok(); let debug = radicle::profile::env::debug();
let stdin = io::stdin(); let stdin = io::stdin();
let mut line = String::new(); let mut line = String::new();

View File

@ -19,27 +19,6 @@ use crate::storage::ReadRepository;
pub struct Timestamp(LocalTime); pub struct Timestamp(LocalTime);
impl Timestamp { impl Timestamp {
/// Construct a `Timestamp` corresponding to the current time.
///
/// # Note
///
/// If this is used in debug mode, `RAD_COMMIT_TIME` will be used
/// to construct the timestamp.
pub fn now() -> Self {
if cfg!(debug_assertions) {
if let Ok(s) = std::env::var("RAD_COMMIT_TIME") {
// SAFETY: Only used in test code.
#[allow(clippy::unwrap_used)]
let secs = s.trim().parse::<u64>().unwrap();
Self::from_secs(secs)
} else {
Self(LocalTime::now())
}
} else {
Self(LocalTime::now())
}
}
pub fn from_secs(secs: u64) -> Self { pub fn from_secs(secs: u64) -> Self {
Self(LocalTime::from_secs(secs)) Self(LocalTime::from_secs(secs))
} }

View File

@ -2686,6 +2686,7 @@ mod test {
use crate::crypto::test::signer::MockSigner; use crate::crypto::test::signer::MockSigner;
use crate::identity; use crate::identity;
use crate::patch::cache::Patches as _; use crate::patch::cache::Patches as _;
use crate::profile::env;
use crate::test; use crate::test;
use crate::test::arbitrary; use crate::test::arbitrary;
use crate::test::arbitrary::gen; use crate::test::arbitrary::gen;
@ -2996,7 +2997,7 @@ mod test {
let base = arbitrary::oid(); let base = arbitrary::oid();
let oid = arbitrary::oid(); let oid = arbitrary::oid();
let repo = gen::<MockRepository>(1); let repo = gen::<MockRepository>(1);
let time = Timestamp::now(); let time = env::local_time();
let alice = MockSigner::default(); let alice = MockSigner::default();
let bob = MockSigner::default(); let bob = MockSigner::default();
let mut h0: cob::test::HistoryBuilder<Patch> = cob::test::history( let mut h0: cob::test::HistoryBuilder<Patch> = cob::test::history(
@ -3012,7 +3013,7 @@ mod test {
target: MergeTarget::Delegates, target: MergeTarget::Delegates,
}, },
], ],
time, time.into(),
&alice, &alice,
); );
let r1 = h0.commit( let r1 = h0.commit(

View File

@ -689,11 +689,12 @@ mod tests {
use crate::cob::cache::{Store, Update, Write}; use crate::cob::cache::{Store, Update, Write};
use crate::cob::thread::{Comment, Thread}; use crate::cob::thread::{Comment, Thread};
use crate::cob::{Author, Timestamp}; use crate::cob::Author;
use crate::patch::{ use crate::patch::{
ByRevision, MergeTarget, Patch, PatchCounts, PatchId, Revision, RevisionId, State, Status, ByRevision, MergeTarget, Patch, PatchCounts, PatchId, Revision, RevisionId, State, Status,
}; };
use crate::prelude::Did; use crate::prelude::Did;
use crate::profile::env;
use crate::test::arbitrary; use crate::test::arbitrary;
use crate::test::storage::MockRepository; use crate::test::storage::MockRepository;
@ -709,14 +710,14 @@ mod tests {
let description = arbitrary::gen::<String>(1); let description = arbitrary::gen::<String>(1);
let base = arbitrary::oid(); let base = arbitrary::oid();
let oid = arbitrary::oid(); let oid = arbitrary::oid();
let timestamp = Timestamp::now(); let timestamp = env::local_time();
let resolves = BTreeSet::new(); let resolves = BTreeSet::new();
let mut revision = Revision::new( let mut revision = Revision::new(
Author { id: author }, Author { id: author },
description, description,
base, base,
oid, oid,
timestamp, timestamp.into(),
resolves, resolves,
); );
let comment = Comment::new( let comment = Comment::new(
@ -725,7 +726,7 @@ mod tests {
None, None,
None, None,
vec![], vec![],
Timestamp::now(), timestamp.into(),
); );
let thread = Thread::new(arbitrary::oid(), comment); let thread = Thread::new(arbitrary::oid(), comment);
revision.discussion = thread; revision.discussion = thread;

View File

@ -18,6 +18,7 @@ use crate::git::ext::commit::headers::Headers;
use crate::git::ext::commit::{trailers::OwnedTrailer, Commit}; use crate::git::ext::commit::{trailers::OwnedTrailer, Commit};
use crate::git::Oid; use crate::git::Oid;
use crate::prelude::Did; use crate::prelude::Did;
use crate::profile::env;
use crate::storage::ReadRepository; use crate::storage::ReadRepository;
use crate::test::arbitrary; use crate::test::arbitrary;
@ -203,9 +204,9 @@ impl<G: Signer> Actor<G> {
T::Action: Clone + Serialize, T::Action: Clone + Serialize,
{ {
let identity = arbitrary::oid(); let identity = arbitrary::oid();
let timestamp = Timestamp::now(); let timestamp = env::commit_time();
self.op_with::<T>(actions, Some(identity), timestamp) self.op_with::<T>(actions, Some(identity), timestamp.into())
} }
/// Get the actor's DID. /// Get the actor's DID.

View File

@ -632,6 +632,7 @@ mod tests {
use crate::cob::test; use crate::cob::test;
use crate::crypto::test::signer::MockSigner; use crate::crypto::test::signer::MockSigner;
use crate::crypto::Signer; use crate::crypto::Signer;
use crate::profile::env;
use crate::test::arbitrary; use crate::test::arbitrary;
use crate::test::arbitrary::gen; use crate::test::arbitrary::gen;
use crate::test::storage::MockRepository; use crate::test::storage::MockRepository;
@ -743,14 +744,14 @@ mod tests {
let bob = MockSigner::default(); let bob = MockSigner::default();
let eve = MockSigner::default(); let eve = MockSigner::default();
let repo = gen::<MockRepository>(1); let repo = gen::<MockRepository>(1);
let time = Timestamp::now(); let time = env::local_time();
let mut a = test::history::<Thread, _>( let mut a = test::history::<Thread, _>(
&[Action::Comment { &[Action::Comment {
body: "Thread root".to_owned(), body: "Thread root".to_owned(),
reply_to: None, reply_to: None,
}], }],
time, time.into(),
&alice, &alice,
); );
a.comment("Alice comment", Some(*a.root().id()), &alice); a.comment("Alice comment", Some(*a.root().id()), &alice);
@ -810,14 +811,14 @@ mod tests {
let repo = gen::<MockRepository>(1); let repo = gen::<MockRepository>(1);
let alice = MockSigner::default(); let alice = MockSigner::default();
let bob = MockSigner::default(); let bob = MockSigner::default();
let time = Timestamp::now(); let time = env::local_time();
let mut a = test::history::<Thread, _>( let mut a = test::history::<Thread, _>(
&[Action::Comment { &[Action::Comment {
body: "Thread root".to_owned(), body: "Thread root".to_owned(),
reply_to: None, reply_to: None,
}], }],
time, time.into(),
&alice, &alice,
); );
let mut b = a.clone(); let mut b = a.clone();

View File

@ -46,7 +46,3 @@ pub mod prelude {
BranchName, ReadRepository, ReadStorage, SignRepository, WriteRepository, WriteStorage, BranchName, ReadRepository, ReadStorage, SignRepository, WriteRepository, WriteStorage,
}; };
} }
pub mod env {
pub use crypto::env::*;
}

View File

@ -42,8 +42,38 @@ pub mod env {
pub const RAD_PASSPHRASE: &str = "RAD_PASSPHRASE"; pub const RAD_PASSPHRASE: &str = "RAD_PASSPHRASE";
/// RNG seed. Must be convertible to a `u64`. /// RNG seed. Must be convertible to a `u64`.
pub const RAD_RNG_SEED: &str = "RAD_RNG_SEED"; pub const RAD_RNG_SEED: &str = "RAD_RNG_SEED";
/// Private key seed. Used for generating deterministic keypairs.
pub const RAD_SEED: &str = "RAD_SEED";
/// Show radicle hints. /// Show radicle hints.
pub const RAD_HINT: &str = "RAD_HINT"; pub const RAD_HINT: &str = "RAD_HINT";
/// Environment variable to set to overwrite the commit date for both
/// the author and the committer.
///
/// The format must be a unix timestamp.
pub const RAD_COMMIT_TIME: &str = "RAD_COMMIT_TIME";
/// Override the device's local time.
/// The format must be a unix timestamp.
pub const RAD_LOCAL_TIME: &str = "RAD_LOCAL_TIME";
// Turn debug mode on.
pub const RAD_DEBUG: &str = "RAD_DEBUG";
// Used to set the Git committer timestamp. Can be overridden
// to generate deterministic COB IDs.
pub const GIT_COMMITTER_DATE: &str = "GIT_COMMITTER_DATE";
/// Commit timestamp to use. Can be overriden by [`RAD_COMMIT_TIME`].
pub fn commit_time() -> localtime::LocalTime {
time(RAD_COMMIT_TIME).unwrap_or_else(local_time)
}
/// Local time. Can be overriden by [`RAD_LOCAL_TIME`].
pub fn local_time() -> localtime::LocalTime {
time(RAD_LOCAL_TIME).unwrap_or_else(localtime::LocalTime::now)
}
/// Whether debug mode is on.
pub fn debug() -> bool {
var(RAD_DEBUG).is_ok()
}
/// Whether or not to show hints. /// Whether or not to show hints.
pub fn hints() -> bool { pub fn hints() -> bool {
@ -74,15 +104,47 @@ pub mod env {
/// Get a random number generator from the environment. /// Get a random number generator from the environment.
pub fn rng() -> fastrand::Rng { pub fn rng() -> fastrand::Rng {
if let Ok(seed) = var(RAD_RNG_SEED) { if let Ok(seed) = var(RAD_RNG_SEED) {
return fastrand::Rng::with_seed( let Ok(seed) = seed.parse() else {
seed.parse() panic!("env::rng: invalid seed specified in `{RAD_RNG_SEED}`");
.expect("env::rng: invalid seed specified in `RAD_RNG_SEED`"), };
); fastrand::Rng::with_seed(seed)
} } else {
fastrand::Rng::new() fastrand::Rng::new()
} }
} }
/// Return the seed stored in the [`RAD_SEED`] environment variable, or generate a random one.
pub fn seed() -> crypto::Seed {
if let Ok(seed) = var(RAD_SEED) {
let Ok(seed) = (0..seed.len())
.step_by(2)
.map(|i| u8::from_str_radix(&seed[i..i + 2], 16))
.collect::<Result<Vec<u8>, _>>()
else {
panic!("env::seed: invalid hexadecimal value set in `{RAD_SEED}`");
};
let Ok(seed): Result<[u8; 32], _> = seed.try_into() else {
panic!("env::seed: invalid seed length set in `{RAD_SEED}`");
};
crypto::Seed::new(seed)
} else {
crypto::Seed::generate()
}
}
fn time(key: &str) -> Option<localtime::LocalTime> {
if let Ok(s) = var(key) {
match s.trim().parse::<u64>() {
Ok(t) => return Some(localtime::LocalTime::from_secs(t)),
Err(e) => {
panic!("env::time: invalid value {s:?} for `{key}` environment variable: {e}");
}
}
}
None
}
}
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum Error { pub enum Error {
#[error(transparent)] #[error(transparent)]
@ -206,9 +268,14 @@ pub struct Profile {
} }
impl Profile { impl Profile {
pub fn init(home: Home, alias: Alias, passphrase: Option<Passphrase>) -> Result<Self, Error> { pub fn init(
home: Home,
alias: Alias,
passphrase: Option<Passphrase>,
seed: crypto::Seed,
) -> Result<Self, Error> {
let keystore = Keystore::new(&home.keys()); let keystore = Keystore::new(&home.keys());
let public_key = keystore.init("radicle", passphrase)?; let public_key = keystore.init("radicle", passphrase, seed)?;
let config = Config::init(alias.clone(), home.config().as_path())?; let config = Config::init(alias.clone(), home.config().as_path())?;
let storage = Storage::open( let storage = Storage::open(
home.storage(), home.storage(),

View File

@ -14,6 +14,7 @@ use thiserror::Error;
use crate::git; use crate::git;
use crate::git::ext as git_ext; use crate::git::ext as git_ext;
use crate::git::Oid; use crate::git::Oid;
use crate::profile::env;
use crate::storage; use crate::storage;
use crate::storage::{ReadRepository, RemoteId, WriteRepository}; use crate::storage::{ReadRepository, RemoteId, WriteRepository};
@ -309,17 +310,17 @@ impl SignedRefs<Verified> {
}?; }?;
let sigref = sigref.with_namespace(remote.into()); let sigref = sigref.with_namespace(remote.into());
let author = raw.signature()?; let author = if let Ok(s) = env::var(env::GIT_COMMITTER_DATE) {
let Ok(timestamp) = s.trim().parse::<i64>() else {
#[cfg(debug_assertions)] panic!(
let author = if let Ok(s) = std::env::var("RAD_COMMIT_TIME") { "Invalid timestamp value {s:?} for `{}`",
// SAFETY: Only used in test code. env::GIT_COMMITTER_DATE
#[allow(clippy::unwrap_used)] );
let timestamp = s.trim().parse::<i64>().unwrap(); };
let time = git2::Time::new(timestamp, 0); let time = git2::Time::new(timestamp, 0);
git2::Signature::new("radicle", remote.to_string().as_str(), &time)? git2::Signature::new("radicle", remote.to_string().as_str(), &time)?
} else { } else {
author raw.signature()?
}; };
let commit = raw.commit( let commit = raw.commit(