open_envault/crypto/
mod.rs1pub mod keys;
9pub mod store;
10pub mod util;
11pub mod value;
12
13use anyhow::{Context, Result};
14use std::{
15 env, fs,
16 path::{Path, PathBuf},
17};
18
19pub use keys::{Identity, Recipient};
20
21pub fn default_key_dir() -> PathBuf {
23 env::var_os("XDG_CONFIG_HOME")
24 .map(PathBuf::from)
25 .or_else(|| env::var_os("APPDATA").map(PathBuf::from))
26 .unwrap_or_else(|| {
27 PathBuf::from(env::var("HOME").unwrap_or_else(|_| ".".into())).join(".config")
28 })
29 .join("open-envault")
30 .join("keys")
31}
32
33fn legacy_key_dir() -> PathBuf {
34 default_key_dir()
35 .parent()
36 .unwrap_or_else(|| Path::new("."))
37 .join("open_envault")
38 .join("keys")
39}
40
41fn atomic_write(path: &Path, content: &[u8]) -> Result<()> {
43 let parent = path.parent().unwrap_or_else(|| Path::new("."));
44 fs::create_dir_all(parent)?;
45 let file_name = path
46 .file_name()
47 .map(|n| n.to_string_lossy().into_owned())
48 .unwrap_or_else(|| "out".into());
49 let tmp = parent.join(format!(
50 ".{file_name}.{}.{}.tmp",
51 std::process::id(),
52 std::time::SystemTime::now()
53 .duration_since(std::time::UNIX_EPOCH)
54 .unwrap_or_default()
55 .as_nanos()
56 ));
57 let mut file = fs::OpenOptions::new()
58 .write(true)
59 .create_new(true)
60 .open(&tmp)?;
61 std::io::Write::write_all(&mut file, content)?;
62 file.sync_all()?;
63 drop(file);
64 replace_file(&tmp, path).inspect_err(|_error| {
65 let _ = fs::remove_file(&tmp);
66 })?;
67 #[cfg(unix)]
68 fs::File::open(parent)?.sync_all()?;
69 Ok(())
70}
71
72#[cfg(not(windows))]
73fn replace_file(source: &Path, destination: &Path) -> std::io::Result<()> {
74 fs::rename(source, destination)
75}
76
77#[cfg(windows)]
78fn replace_file(source: &Path, destination: &Path) -> std::io::Result<()> {
79 use std::os::windows::ffi::OsStrExt;
80 use windows_sys::Win32::Storage::FileSystem::{MOVEFILE_REPLACE_EXISTING, MoveFileExW};
81 let source: Vec<u16> = source.as_os_str().encode_wide().chain(Some(0)).collect();
82 let destination: Vec<u16> = destination
83 .as_os_str()
84 .encode_wide()
85 .chain(Some(0))
86 .collect();
87 if unsafe {
88 MoveFileExW(
89 source.as_ptr(),
90 destination.as_ptr(),
91 MOVEFILE_REPLACE_EXISTING,
92 )
93 } == 0
94 {
95 Err(std::io::Error::last_os_error())
96 } else {
97 Ok(())
98 }
99}
100
101pub fn encrypt(content: &str, path: &Path, recipients: &[String]) -> Result<()> {
103 let text = store::encrypt(content, &store::recipients_from_strings(recipients)?)?;
104 atomic_write(path, text.as_bytes())
105}
106
107pub fn decrypt_for(path: &Path, environment: Option<&str>) -> Result<String> {
110 decrypt_for_with_key_file(path, environment, None)
111}
112
113pub fn decrypt_for_with_key_file(
115 path: &Path,
116 environment: Option<&str>,
117 configured_key_file: Option<&Path>,
118) -> Result<String> {
119 let mut identities = keys::identities_from_env();
120 if let Some(key_path) = configured_key_file
121 && key_path.is_file()
122 {
123 identities.push(keys::read_identity_file(key_path)?);
124 }
125 if let Some(name) = environment {
126 for key_dir in [default_key_dir(), legacy_key_dir()] {
127 let key_path = key_dir.join(format!("{name}.txt"));
128 if key_path.is_file()
129 && let Ok(identity) = keys::read_identity_file(&key_path)
130 {
131 identities.push(identity);
132 break;
133 }
134 }
135 }
136 if identities.is_empty() {
137 anyhow::bail!(
138 "no age identity available; set OPENENVAULT_AGE_KEY or SOPS_AGE_KEY, configure a key file, or run `oenv setup`"
139 );
140 }
141 let text = fs::read_to_string(path)
142 .with_context(|| format!("read encrypted file {}", path.display()))?;
143 store::decrypt(&text, &identities)
144}
145
146pub fn decrypt(path: &Path) -> Result<String> {
148 decrypt_for(path, None)
149}
150
151pub fn recipients_of(path: &Path) -> Result<Vec<String>> {
153 let text = fs::read_to_string(path)
154 .with_context(|| format!("read encrypted file {}", path.display()))?;
155 store::list_recipients(&text)
156}
157
158pub fn generate_key(path: &Path) -> Result<String> {
161 let (text, _public) = keys::generate_identity()?;
162 if let Some(parent) = path.parent() {
163 fs::create_dir_all(parent)?;
164 }
165 let mut file = fs::OpenOptions::new()
166 .write(true)
167 .create_new(true)
168 .open(path)
169 .with_context(|| {
170 if path.exists() {
171 format!(
172 "key file already exists; refusing to overwrite {}",
173 path.display()
174 )
175 } else {
176 format!("write key file {}", path.display())
177 }
178 })?;
179 std::io::Write::write_all(&mut file, text.as_bytes())?;
180 file.sync_all()?;
181 #[cfg(unix)]
182 {
183 use std::os::unix::fs::PermissionsExt;
184 fs::set_permissions(path, fs::Permissions::from_mode(0o600))
185 .with_context(|| format!("chmod key file {}", path.display()))?;
186 }
187 Ok(text)
188}
189
190pub fn ensure_key(path: &Path) -> Result<(String, String, bool)> {
192 if path.is_file() {
193 let identity = keys::read_identity_file(path)
194 .with_context(|| format!("validate existing age key {}", path.display()))?;
195 return Ok((
196 identity.to_public().to_string(),
197 path.display().to_string(),
198 false,
199 ));
200 }
201 let text = generate_key(path)?;
202 let identity = keys::identity_from_key_text(&text)?;
203 Ok((
204 identity.to_public().to_string(),
205 path.display().to_string(),
206 true,
207 ))
208}