Skip to main content

nym_pemstore/
lib.rs

1// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::traits::{PemStorableKey, PemStorableKeyPair};
5use pem::Pem;
6use std::fs::File;
7use std::io::{self, Read, Write};
8use std::ops::Deref;
9use std::path::{Path, PathBuf};
10use tracing::debug;
11use zeroize::{Zeroize, Zeroizing};
12
13pub mod traits;
14
15struct ZeroizingPem(Pem);
16
17impl Zeroize for ZeroizingPem {
18    fn zeroize(&mut self) {
19        self.0.tag.zeroize();
20        self.0.contents.zeroize();
21    }
22}
23impl Drop for ZeroizingPem {
24    fn drop(&mut self) {
25        self.zeroize();
26    }
27}
28
29impl Deref for ZeroizingPem {
30    type Target = Pem;
31    fn deref(&self) -> &Self::Target {
32        &self.0
33    }
34}
35
36#[derive(Debug, Clone, Default)]
37pub struct KeyPairPath {
38    pub private_key_path: PathBuf,
39    pub public_key_path: PathBuf,
40}
41
42impl KeyPairPath {
43    pub fn new<P: AsRef<Path>>(private_key_path: P, public_key_path: P) -> Self {
44        KeyPairPath {
45            private_key_path: private_key_path.as_ref().to_owned(),
46            public_key_path: public_key_path.as_ref().to_owned(),
47        }
48    }
49}
50
51pub fn load_keypair<T>(paths: &KeyPairPath) -> io::Result<T>
52where
53    T: PemStorableKeyPair,
54{
55    let private: T::PrivatePemKey = load_key(&paths.private_key_path)?;
56    let public: T::PublicPemKey = load_key(&paths.public_key_path)?;
57    Ok(T::from_keys(private, public))
58}
59
60pub fn store_keypair<T>(keypair: &T, paths: &KeyPairPath) -> io::Result<()>
61where
62    T: PemStorableKeyPair,
63{
64    store_key(keypair.public_key(), &paths.public_key_path)?;
65    store_key(keypair.private_key(), &paths.private_key_path)
66}
67
68pub fn load_key<T, P>(path: P) -> io::Result<T>
69where
70    T: PemStorableKey,
71    P: AsRef<Path>,
72{
73    debug!(
74        "attempting to load key with the following pem type: {}",
75        T::pem_type()
76    );
77    let key_pem = read_pem_file(path)?;
78
79    if T::pem_type() != key_pem.tag {
80        return Err(io::Error::other(format!(
81            "unexpected key pem tag. Got '{}', expected: '{}'",
82            key_pem.0.tag,
83            T::pem_type()
84        )));
85    }
86
87    let key = match T::from_bytes(&key_pem.contents) {
88        Ok(key) => key,
89        Err(err) => return Err(io::Error::new(io::ErrorKind::InvalidData, err.to_string())),
90    };
91
92    Ok(key)
93}
94
95pub fn store_key<T, P>(key: &T, path: P) -> io::Result<()>
96where
97    T: PemStorableKey,
98    P: AsRef<Path>,
99{
100    write_pem_file(path, key.to_bytes(), T::pem_type())
101}
102
103fn read_pem_file<P: AsRef<Path>>(filepath: P) -> io::Result<ZeroizingPem> {
104    let mut pem_bytes = File::open(filepath)?;
105    let mut buf = Zeroizing::new(Vec::new());
106    pem_bytes.read_to_end(&mut buf)?;
107    pem::parse(&buf).map(ZeroizingPem).map_err(io::Error::other)
108}
109
110fn write_pem_file<P: AsRef<Path>>(filepath: P, mut data: Vec<u8>, tag: &str) -> io::Result<()> {
111    // ensure the whole directory structure exists
112    // don't use nested if else due to contracts still being on 2021 and this code being pulled in indirectly in tests
113    #[allow(clippy::collapsible_if)]
114    if let Some(parent_dir) = filepath.as_ref().parent() {
115        if let Err(err) = std::fs::create_dir_all(parent_dir) {
116            // in case of a failure, make sure to zeroize the data before returning
117            // (we can't wrap it in `Zeroize` due to `Pem` requirements)
118            data.zeroize();
119            return Err(err);
120        }
121    }
122
123    let mut file = File::create(filepath.as_ref())?;
124
125    let pem = ZeroizingPem(Pem {
126        tag: tag.to_string(),
127        contents: data,
128    });
129    let key = Zeroizing::new(pem::encode(&pem));
130    file.write_all(key.as_bytes())?;
131
132    // note: this is only supported on unix (on different systems, like Windows, it will just
133    // be ignored)
134    // TODO: a possible consideration would be to use `permission.set_readonly(true)`,
135    // which would work on both platforms, but that would leave keys on unix with 0444,
136    // which I feel is too open.
137    #[cfg(target_family = "unix")]
138    {
139        use std::fs;
140        use std::os::unix::fs::PermissionsExt;
141
142        let mut permissions = file.metadata()?.permissions();
143        permissions.set_mode(0o600);
144        fs::set_permissions(filepath, permissions)?;
145    }
146
147    Ok(())
148}