1use std::{
2 fs::OpenOptions,
3 io::Write as _,
4 path::{Path, PathBuf},
5};
6
7use chacha20poly1305::{
8 XChaCha20Poly1305, XNonce,
9 aead::{Aead as _, KeyInit as _, Payload},
10};
11use hkdf::Hkdf;
12use iroh_db_core::DomainId;
13use sha2::Sha256;
14use zeroize::Zeroizing;
15
16use crate::{DomainKey, IrohSigner, SecurityError};
17
18const MASTER_FILE: &str = "master.key";
19const CREDENTIALS_FILE: &str = "credentials.v1";
20const MAGIC: &[u8; 8] = b"IDBKEY01";
21const DOMAIN_MAGIC: &[u8; 8] = b"IDBDOM01";
22const PLAINTEXT_LEN: usize = 96;
23const DOMAIN_PLAINTEXT_LEN: usize = 72;
24const HEX: &[u8; 16] = b"0123456789abcdef";
25
26#[derive(Clone)]
28pub struct DomainSeed {
29 domain_id: DomainId,
30 epoch: u64,
31 domain_key: DomainKey,
32}
33
34impl DomainSeed {
35 pub fn generate() -> Result<Self, SecurityError> {
37 let mut domain_id = [0_u8; 32];
38 getrandom::fill(&mut domain_id).map_err(|_| SecurityError::RandomnessUnavailable)?;
39 Ok(Self {
40 domain_id: DomainId::from_bytes(domain_id),
41 epoch: 0,
42 domain_key: DomainKey::generate()?,
43 })
44 }
45
46 pub const fn domain_id(&self) -> DomainId {
48 self.domain_id
49 }
50
51 pub const fn epoch(&self) -> u64 {
53 self.epoch
54 }
55
56 pub fn domain_key(&self) -> DomainKey {
58 self.domain_key.clone()
59 }
60
61 #[doc(hidden)]
63 pub const fn epoch_key_bytes(&self) -> [u8; 32] {
64 *self.domain_key.as_bytes()
65 }
66
67 pub fn next_epoch(&self) -> Result<Self, SecurityError> {
69 Ok(Self {
70 domain_id: self.domain_id,
71 epoch: self
72 .epoch
73 .checked_add(1)
74 .ok_or(SecurityError::InvalidKeyProviderData)?,
75 domain_key: DomainKey::generate()?,
76 })
77 }
78
79 pub fn distribution_digest(&self) -> [u8; 32] {
81 let mut input = Vec::with_capacity(72);
82 input.extend_from_slice(self.domain_id.as_bytes());
83 input.extend_from_slice(&self.epoch.to_be_bytes());
84 input.extend_from_slice(self.domain_key.as_bytes());
85 *blake3::hash(&input).as_bytes()
86 }
87
88 pub(crate) const fn from_parts(domain_id: DomainId, epoch: u64, domain_key: DomainKey) -> Self {
89 Self {
90 domain_id,
91 epoch,
92 domain_key,
93 }
94 }
95
96 #[doc(hidden)]
98 pub const fn from_epoch_key(domain_id: DomainId, epoch: u64, key: [u8; 32]) -> Self {
99 Self::from_parts(domain_id, epoch, DomainKey::from_bytes(key))
100 }
101}
102
103impl std::fmt::Debug for DomainSeed {
104 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105 formatter.write_str("DomainSeed([redacted])")
106 }
107}
108
109#[derive(Clone)]
111pub struct LocalCredentials {
112 signer: IrohSigner,
113 domain_key: DomainKey,
114 domain_id: DomainId,
115 root: PathBuf,
116}
117
118pub trait KeyProvider: Send + Sync {
123 fn signer(&self) -> IrohSigner;
125 fn endpoint_secret_key(&self) -> iroh_base::SecretKey;
127 fn domain_id(&self) -> DomainId;
129 fn domain_seed(&self) -> DomainSeed;
131 fn domain_key(&self) -> DomainKey {
133 self.domain_seed().domain_key()
134 }
135 fn store_domain(&self, seed: &DomainSeed) -> Result<(), SecurityError>;
137 fn stage_domain(&self, seed: &DomainSeed) -> Result<(), SecurityError>;
139 fn activate_domain(&self, seed: &DomainSeed) -> Result<(), SecurityError>;
141 fn load_domain(&self, domain_id: DomainId) -> Result<DomainSeed, SecurityError>;
143 fn load_domain_epoch(
145 &self,
146 domain_id: DomainId,
147 epoch: u64,
148 ) -> Result<DomainSeed, SecurityError>;
149 fn domains(&self) -> Result<Vec<DomainId>, SecurityError>;
151}
152
153impl LocalCredentials {
154 pub fn load(root: impl AsRef<Path>) -> Result<Self, SecurityError> {
156 let root = root.as_ref();
157 let master = load_master(&root.join(MASTER_FILE))?;
158 let credentials_path = root.join(CREDENTIALS_FILE);
159 check_owner_only(&credentials_path)?;
160 let bytes = std::fs::read(credentials_path).map_err(io_error)?;
161 let mut credentials = decode_credentials(&bytes, &master)?;
162 credentials.root = root.to_path_buf();
163 Ok(credentials)
164 }
165
166 pub fn load_or_create(root: impl AsRef<Path>) -> Result<Self, SecurityError> {
168 let root = root.as_ref();
169 std::fs::create_dir_all(root).map_err(io_error)?;
170 let master_path = root.join(MASTER_FILE);
171 let master = load_or_create_master(&master_path)?;
172 let credentials_path = root.join(CREDENTIALS_FILE);
173 if credentials_path.exists() {
174 check_owner_only(&credentials_path)?;
175 let bytes = std::fs::read(credentials_path).map_err(io_error)?;
176 let mut credentials = decode_credentials(&bytes, &master)?;
177 credentials.root = root.to_path_buf();
178 Ok(credentials)
179 } else {
180 let credentials = Self::generate(root.to_path_buf())?;
181 let bytes = encode_credentials(&credentials, &master)?;
182 write_owner_only(&credentials_path, &bytes)?;
183 Ok(credentials)
184 }
185 }
186
187 pub fn create_for_domain(
189 root: impl AsRef<Path>,
190 seed: &DomainSeed,
191 ) -> Result<Self, SecurityError> {
192 let root = root.as_ref();
193 std::fs::create_dir_all(root).map_err(io_error)?;
194 let master = load_or_create_master(&root.join(MASTER_FILE))?;
195 let credentials_path = root.join(CREDENTIALS_FILE);
196 if credentials_path.exists() {
197 return Err(SecurityError::InvalidKeyProviderData);
198 }
199 let credentials = Self {
200 signer: IrohSigner::generate(),
201 domain_key: seed.domain_key(),
202 domain_id: seed.domain_id(),
203 root: root.to_path_buf(),
204 };
205 write_owner_only(
206 &credentials_path,
207 &encode_credentials(&credentials, &master)?,
208 )?;
209 Ok(credentials)
210 }
211
212 pub fn signer(&self) -> IrohSigner {
214 self.signer.clone()
215 }
216
217 pub fn domain_key(&self) -> DomainKey {
219 self.domain_key.clone()
220 }
221
222 pub const fn domain_id(&self) -> DomainId {
224 self.domain_id
225 }
226
227 pub fn domain_seed(&self) -> DomainSeed {
229 DomainSeed {
230 domain_id: self.domain_id,
231 epoch: 0,
232 domain_key: self.domain_key.clone(),
233 }
234 }
235
236 pub fn store_domain(&self, seed: &DomainSeed) -> Result<(), SecurityError> {
238 let directory = self.root.join("domains");
239 std::fs::create_dir_all(&directory).map_err(io_error)?;
240 let path = directory.join(format!("{}.v1", seed.domain_id));
241 if path.exists() {
242 let existing = self.load_domain(seed.domain_id)?;
243 if existing.epoch == seed.epoch
244 && existing.domain_key.as_bytes() == seed.domain_key.as_bytes()
245 {
246 return Ok(());
247 }
248 return Err(SecurityError::InvalidKeyProviderData);
249 }
250 let master = load_or_create_master(&self.root.join(MASTER_FILE))?;
251 let bytes = encode_domain_seed(seed, &master)?;
252 store_epoch_archive(&directory, seed, &master)?;
253 write_owner_only(&path, &bytes)
254 }
255
256 pub fn replace_domain(&self, seed: &DomainSeed) -> Result<(), SecurityError> {
258 let current = self.load_domain(seed.domain_id)?;
259 if seed.epoch
260 != current
261 .epoch
262 .checked_add(1)
263 .ok_or(SecurityError::InvalidKeyProviderData)?
264 {
265 return Err(SecurityError::InvalidKeyProviderData);
266 }
267 let directory = self.root.join("domains");
268 std::fs::create_dir_all(&directory).map_err(io_error)?;
269 let master = load_or_create_master(&self.root.join(MASTER_FILE))?;
270 store_epoch_archive(&directory, ¤t, &master)?;
271 store_epoch_archive(&directory, seed, &master)?;
272 let bytes = encode_domain_seed(seed, &master)?;
273 write_owner_only_replace(&directory.join(format!("{}.v1", seed.domain_id)), &bytes)
274 }
275
276 #[doc(hidden)]
278 pub fn stage_domain(&self, seed: &DomainSeed) -> Result<(), SecurityError> {
279 let current = self.load_domain(seed.domain_id)?;
280 if seed.epoch == current.epoch
281 && seed.domain_key.as_bytes() == current.domain_key.as_bytes()
282 {
283 return Ok(());
284 }
285 if seed.epoch
286 != current
287 .epoch
288 .checked_add(1)
289 .ok_or(SecurityError::InvalidKeyProviderData)?
290 {
291 return Err(SecurityError::InvalidKeyProviderData);
292 }
293 let directory = self.root.join("domains");
294 std::fs::create_dir_all(&directory).map_err(io_error)?;
295 let master = load_or_create_master(&self.root.join(MASTER_FILE))?;
296 store_epoch_archive(&directory, seed, &master)
297 }
298
299 #[doc(hidden)]
301 pub fn activate_domain(&self, seed: &DomainSeed) -> Result<(), SecurityError> {
302 let directory = self.root.join("domains");
303 std::fs::create_dir_all(&directory).map_err(io_error)?;
304 let master = load_or_create_master(&self.root.join(MASTER_FILE))?;
305 store_epoch_archive(&directory, seed, &master)?;
306 write_owner_only_replace(
307 &directory.join(format!("{}.v1", seed.domain_id)),
308 &encode_domain_seed(seed, &master)?,
309 )
310 }
311
312 pub fn load_domain(&self, domain_id: DomainId) -> Result<DomainSeed, SecurityError> {
314 if domain_id == self.domain_id {
315 let catalog = self.root.join("domains").join(format!("{domain_id}.v1"));
316 if !catalog.exists() {
317 return Ok(self.domain_seed());
318 }
319 }
320 let path = self.root.join("domains").join(format!("{domain_id}.v1"));
321 check_owner_only(&path)?;
322 let master = load_or_create_master(&self.root.join(MASTER_FILE))?;
323 let bytes = std::fs::read(path).map_err(io_error)?;
324 let seed = decode_domain_seed(&bytes, &master)?;
325 if seed.domain_id != domain_id {
326 return Err(SecurityError::InvalidKeyProviderData);
327 }
328 Ok(seed)
329 }
330
331 pub fn load_domain_epoch(
333 &self,
334 domain_id: DomainId,
335 epoch: u64,
336 ) -> Result<DomainSeed, SecurityError> {
337 let current = self.load_domain(domain_id)?;
338 if current.epoch == epoch {
339 return Ok(current);
340 }
341 if domain_id == self.domain_id && epoch == 0 {
342 return Ok(self.domain_seed());
343 }
344 let path = self
345 .root
346 .join("domains")
347 .join(format!("{domain_id}.epoch-{epoch}.v1"));
348 check_owner_only(&path)?;
349 let master = load_or_create_master(&self.root.join(MASTER_FILE))?;
350 let bytes = std::fs::read(path).map_err(io_error)?;
351 let seed = decode_domain_seed(&bytes, &master)?;
352 if seed.domain_id != domain_id || seed.epoch != epoch {
353 return Err(SecurityError::InvalidKeyProviderData);
354 }
355 Ok(seed)
356 }
357
358 pub fn domains(&self) -> Result<Vec<DomainId>, SecurityError> {
360 let mut domains = vec![self.domain_id];
361 let directory = self.root.join("domains");
362 if directory.exists() {
363 for entry in std::fs::read_dir(directory).map_err(io_error)? {
364 let entry = entry.map_err(io_error)?;
365 let name = entry.file_name();
366 let Some(name) = name.to_str() else {
367 continue;
368 };
369 let Some(id) = name.strip_suffix(".v1") else {
370 continue;
371 };
372 let Ok(id) = id.parse() else {
373 continue;
374 };
375 domains.push(id);
376 }
377 }
378 domains.sort_unstable();
379 domains.dedup();
380 Ok(domains)
381 }
382
383 pub fn endpoint_secret_key(&self) -> iroh_base::SecretKey {
385 iroh_base::SecretKey::from_bytes(&self.signer.secret_bytes())
386 }
387
388 fn generate(root: PathBuf) -> Result<Self, SecurityError> {
389 let mut signer = [0_u8; 32];
390 let mut domain_key = [0_u8; 32];
391 let mut domain_id = [0_u8; 32];
392 getrandom::fill(&mut signer).map_err(|_| SecurityError::RandomnessUnavailable)?;
393 getrandom::fill(&mut domain_key).map_err(|_| SecurityError::RandomnessUnavailable)?;
394 getrandom::fill(&mut domain_id).map_err(|_| SecurityError::RandomnessUnavailable)?;
395 Ok(Self {
396 signer: IrohSigner::from_secret_bytes(signer),
397 domain_key: DomainKey::from_bytes(domain_key),
398 domain_id: DomainId::from_bytes(domain_id),
399 root,
400 })
401 }
402}
403
404impl std::fmt::Debug for LocalCredentials {
405 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
406 formatter.write_str("LocalCredentials([redacted])")
407 }
408}
409
410impl KeyProvider for LocalCredentials {
411 fn signer(&self) -> IrohSigner {
412 Self::signer(self)
413 }
414
415 fn endpoint_secret_key(&self) -> iroh_base::SecretKey {
416 Self::endpoint_secret_key(self)
417 }
418
419 fn domain_id(&self) -> DomainId {
420 Self::domain_id(self)
421 }
422
423 fn domain_seed(&self) -> DomainSeed {
424 Self::domain_seed(self)
425 }
426
427 fn store_domain(&self, seed: &DomainSeed) -> Result<(), SecurityError> {
428 Self::store_domain(self, seed)
429 }
430
431 fn stage_domain(&self, seed: &DomainSeed) -> Result<(), SecurityError> {
432 Self::stage_domain(self, seed)
433 }
434
435 fn activate_domain(&self, seed: &DomainSeed) -> Result<(), SecurityError> {
436 Self::activate_domain(self, seed)
437 }
438
439 fn load_domain(&self, domain_id: DomainId) -> Result<DomainSeed, SecurityError> {
440 Self::load_domain(self, domain_id)
441 }
442
443 fn load_domain_epoch(
444 &self,
445 domain_id: DomainId,
446 epoch: u64,
447 ) -> Result<DomainSeed, SecurityError> {
448 Self::load_domain_epoch(self, domain_id, epoch)
449 }
450
451 fn domains(&self) -> Result<Vec<DomainId>, SecurityError> {
452 Self::domains(self)
453 }
454}
455
456fn load_or_create_master(path: &Path) -> Result<Zeroizing<[u8; 32]>, SecurityError> {
457 if path.exists() {
458 return load_master(path);
459 }
460 let mut bytes = Zeroizing::new([0_u8; 32]);
461 getrandom::fill(&mut *bytes).map_err(|_| SecurityError::RandomnessUnavailable)?;
462 write_owner_only(path, &*bytes)?;
463 Ok(bytes)
464}
465
466fn load_master(path: &Path) -> Result<Zeroizing<[u8; 32]>, SecurityError> {
467 check_owner_only(path)?;
468 let bytes = std::fs::read(path).map_err(io_error)?;
469 let bytes: [u8; 32] = bytes
470 .try_into()
471 .map_err(|_| SecurityError::InvalidKeyProviderData)?;
472 Ok(Zeroizing::new(bytes))
473}
474
475fn encode_credentials(
476 credentials: &LocalCredentials,
477 master: &[u8; 32],
478) -> Result<Vec<u8>, SecurityError> {
479 let mut nonce = [0_u8; 24];
480 getrandom::fill(&mut nonce).map_err(|_| SecurityError::RandomnessUnavailable)?;
481 let mut plaintext = Zeroizing::new(Vec::with_capacity(PLAINTEXT_LEN));
482 plaintext.extend_from_slice(&credentials.signer.secret_bytes());
483 plaintext.extend_from_slice(credentials.domain_key.as_bytes());
484 plaintext.extend_from_slice(credentials.domain_id.as_bytes());
485 let key = vault_key(master)?;
486 let cipher = XChaCha20Poly1305::new((&*key).into());
487 let ciphertext = cipher
488 .encrypt(
489 &XNonce::from(nonce),
490 Payload {
491 msg: &plaintext,
492 aad: MAGIC,
493 },
494 )
495 .map_err(|_| SecurityError::EncryptionFailed)?;
496 let mut encoded = Vec::with_capacity(MAGIC.len() + nonce.len() + ciphertext.len());
497 encoded.extend_from_slice(MAGIC);
498 encoded.extend_from_slice(&nonce);
499 encoded.extend_from_slice(&ciphertext);
500 Ok(encoded)
501}
502
503fn decode_credentials(bytes: &[u8], master: &[u8; 32]) -> Result<LocalCredentials, SecurityError> {
504 if bytes.len() != MAGIC.len() + 24 + PLAINTEXT_LEN + 16 || &bytes[..MAGIC.len()] != MAGIC {
505 return Err(SecurityError::InvalidKeyProviderData);
506 }
507 let nonce: [u8; 24] = bytes[8..32]
508 .try_into()
509 .map_err(|_| SecurityError::InvalidKeyProviderData)?;
510 let key = vault_key(master)?;
511 let cipher = XChaCha20Poly1305::new((&*key).into());
512 let plaintext = Zeroizing::new(
513 cipher
514 .decrypt(
515 &XNonce::from(nonce),
516 Payload {
517 msg: &bytes[32..],
518 aad: MAGIC,
519 },
520 )
521 .map_err(|_| SecurityError::InvalidKeyProviderData)?,
522 );
523 if plaintext.len() != PLAINTEXT_LEN {
524 return Err(SecurityError::InvalidKeyProviderData);
525 }
526 let signer = plaintext[..32]
527 .try_into()
528 .map_err(|_| SecurityError::InvalidKeyProviderData)?;
529 let domain_key = plaintext[32..64]
530 .try_into()
531 .map_err(|_| SecurityError::InvalidKeyProviderData)?;
532 let domain_id = plaintext[64..]
533 .try_into()
534 .map_err(|_| SecurityError::InvalidKeyProviderData)?;
535 Ok(LocalCredentials {
536 signer: IrohSigner::from_secret_bytes(signer),
537 domain_key: DomainKey::from_bytes(domain_key),
538 domain_id: DomainId::from_bytes(domain_id),
539 root: PathBuf::new(),
540 })
541}
542
543fn encode_domain_seed(seed: &DomainSeed, master: &[u8; 32]) -> Result<Vec<u8>, SecurityError> {
544 let mut nonce = [0_u8; 24];
545 getrandom::fill(&mut nonce).map_err(|_| SecurityError::RandomnessUnavailable)?;
546 let mut plaintext = Zeroizing::new(Vec::with_capacity(DOMAIN_PLAINTEXT_LEN));
547 plaintext.extend_from_slice(seed.domain_id.as_bytes());
548 plaintext.extend_from_slice(&seed.epoch.to_be_bytes());
549 plaintext.extend_from_slice(seed.domain_key.as_bytes());
550 let key = vault_key(master)?;
551 let ciphertext = XChaCha20Poly1305::new((&*key).into())
552 .encrypt(
553 &XNonce::from(nonce),
554 Payload {
555 msg: &plaintext,
556 aad: DOMAIN_MAGIC,
557 },
558 )
559 .map_err(|_| SecurityError::EncryptionFailed)?;
560 let mut encoded = Vec::with_capacity(32 + ciphertext.len());
561 encoded.extend_from_slice(DOMAIN_MAGIC);
562 encoded.extend_from_slice(&nonce);
563 encoded.extend_from_slice(&ciphertext);
564 Ok(encoded)
565}
566
567fn decode_domain_seed(bytes: &[u8], master: &[u8; 32]) -> Result<DomainSeed, SecurityError> {
568 if bytes.len() != 32 + DOMAIN_PLAINTEXT_LEN + 16 || &bytes[..8] != DOMAIN_MAGIC {
569 return Err(SecurityError::InvalidKeyProviderData);
570 }
571 let nonce: [u8; 24] = bytes[8..32]
572 .try_into()
573 .map_err(|_| SecurityError::InvalidKeyProviderData)?;
574 let key = vault_key(master)?;
575 let plaintext = Zeroizing::new(
576 XChaCha20Poly1305::new((&*key).into())
577 .decrypt(
578 &XNonce::from(nonce),
579 Payload {
580 msg: &bytes[32..],
581 aad: DOMAIN_MAGIC,
582 },
583 )
584 .map_err(|_| SecurityError::InvalidKeyProviderData)?,
585 );
586 let domain_id = plaintext[..32]
587 .try_into()
588 .map_err(|_| SecurityError::InvalidKeyProviderData)?;
589 let epoch = u64::from_be_bytes(
590 plaintext[32..40]
591 .try_into()
592 .map_err(|_| SecurityError::InvalidKeyProviderData)?,
593 );
594 let domain_key = plaintext[40..]
595 .try_into()
596 .map_err(|_| SecurityError::InvalidKeyProviderData)?;
597 Ok(DomainSeed {
598 domain_id: DomainId::from_bytes(domain_id),
599 epoch,
600 domain_key: DomainKey::from_bytes(domain_key),
601 })
602}
603
604fn vault_key(master: &[u8; 32]) -> Result<Zeroizing<[u8; 32]>, SecurityError> {
605 let hkdf = Hkdf::<Sha256>::new(Some(b"iroh-db/local-key-vault/v1"), master);
606 let mut key = Zeroizing::new([0_u8; 32]);
607 hkdf.expand(b"credentials/v1", &mut *key)
608 .map_err(|_| SecurityError::KeyDerivationFailed)?;
609 Ok(key)
610}
611
612fn store_epoch_archive(
613 directory: &Path,
614 seed: &DomainSeed,
615 master: &[u8; 32],
616) -> Result<(), SecurityError> {
617 let path = directory.join(format!("{}.epoch-{}.v1", seed.domain_id, seed.epoch));
618 if path.exists() {
619 let bytes = std::fs::read(&path).map_err(io_error)?;
620 let existing = decode_domain_seed(&bytes, master)?;
621 if existing.domain_id == seed.domain_id
622 && existing.epoch == seed.epoch
623 && existing.domain_key.as_bytes() == seed.domain_key.as_bytes()
624 {
625 return Ok(());
626 }
627 return Err(SecurityError::InvalidKeyProviderData);
628 }
629 write_owner_only_replace(&path, &encode_domain_seed(seed, master)?)
630}
631
632fn write_owner_only(path: &Path, bytes: &[u8]) -> Result<(), SecurityError> {
633 let mut options = OpenOptions::new();
634 options.create_new(true).write(true);
635 #[cfg(unix)]
636 {
637 use std::os::unix::fs::OpenOptionsExt as _;
638 options.mode(0o600);
639 }
640 let mut file = options.open(path).map_err(io_error)?;
641 file.write_all(bytes).map_err(io_error)?;
642 file.sync_all().map_err(io_error)
643}
644
645fn write_owner_only_replace(path: &Path, bytes: &[u8]) -> Result<(), SecurityError> {
646 let mut suffix = [0_u8; 8];
647 getrandom::fill(&mut suffix).map_err(|_| SecurityError::RandomnessUnavailable)?;
648 let mut encoded_suffix = String::with_capacity(16);
649 for byte in suffix {
650 encoded_suffix.push(HEX[usize::from(byte >> 4)] as char);
651 encoded_suffix.push(HEX[usize::from(byte & 0x0f)] as char);
652 }
653 let temporary = path.with_extension(format!("tmp-{encoded_suffix}"));
654 write_owner_only(&temporary, bytes)?;
655 std::fs::rename(&temporary, path).map_err(io_error)?;
656 if let Some(parent) = path.parent() {
657 OpenOptions::new()
658 .read(true)
659 .open(parent)
660 .and_then(|directory| directory.sync_all())
661 .map_err(io_error)?;
662 }
663 Ok(())
664}
665
666#[cfg(unix)]
667fn check_owner_only(path: &Path) -> Result<(), SecurityError> {
668 use std::os::unix::fs::PermissionsExt as _;
669 let mode = std::fs::metadata(path)
670 .map_err(io_error)?
671 .permissions()
672 .mode();
673 if mode & 0o077 != 0 {
674 return Err(SecurityError::InsecureKeyPermissions);
675 }
676 Ok(())
677}
678
679#[cfg(not(unix))]
680fn check_owner_only(_path: &Path) -> Result<(), SecurityError> {
681 Ok(())
682}
683
684#[allow(clippy::needless_pass_by_value)]
685fn io_error(error: std::io::Error) -> SecurityError {
686 SecurityError::KeyProviderIo(error.to_string())
687}