rejson/
lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
#![doc = include_str!("README.md")]

mod crypto;
mod json;
mod kube;
mod map;

use std::path::Path;

use anyhow::Result;
pub use crypto::{Key, KeyPair};
pub use json::SecretsFile;
pub use kube::SecretsManifest;
pub use map::SecretsMap;

const NEW_LINE: &str = "\n";
const CARRIAGE_RETURN: &str = "\r";

/// Returns a transform function that compacts multiline strings into single lines with line
/// break characters. This is useful when adding something like a service account in the EJSON file
/// and having the encrypt function compact it before encryption.
pub fn compact() -> Result<impl Fn(String) -> Result<String>> {
    Ok(|s: String| {
        if s.contains(NEW_LINE) || s.contains(CARRIAGE_RETURN) {
            return Ok(s
                .trim()
                .replace(NEW_LINE, r"\n")
                .replace(CARRIAGE_RETURN, r"\r")
                .to_string());
        }

        Ok(s)
    })
}

/// Returns a transform function for use with [SecretsFile::transform] that will encrypt all eligible
/// values (that aren't already encrypted).
pub fn encrypt(secrets_file: &SecretsFile) -> Result<impl Fn(String) -> Result<String>> {
    let public_key = secrets_file.public_key().unwrap();
    let ephemeral_key = KeyPair::generate()?;
    let encryptor = ephemeral_key.encryptor(public_key)?;

    Ok(move |s: String| {
        // Skip encryption if this value is already an EJSON message.
        if crypto::Message::is_valid(&s) {
            return Ok(s);
        }

        encryptor.encrypt(s)
    })
}

/// Returns a transform that will decrypt incoming values from the supplied secrets file. This is
/// done by creating a [KeyPair] consisting of the public key from the file and the supplied
/// private key.
pub fn decrypt(secrets_file: &SecretsFile, private_key: Key) -> Result<impl Fn(String) -> Result<String>> {
    let public_key = secrets_file.public_key().unwrap();
    let decryptor = KeyPair::new(public_key, private_key).decryptor();

    Ok(move |s: String| {
        if !crypto::Message::is_valid(&s) {
            // Skip decryption for values that aren't encrypted.
            return Ok(s);
        }

        decryptor.decrypt(s)
    })
}

/// Loads the private key from disk, searching for a file named as the public key defined in the
/// secrets file.
pub fn load_private_key(secrets_file: &SecretsFile, keydir: &str) -> Result<Key> {
    let public_key = secrets_file.public_key().unwrap();
    Key::from_file(Path::new(keydir).join(public_key.to_string()))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn compact_transform() -> Result<()> {
        let cases = [
            ("\n\n\n\r\r\n", ""),
            ("some\nstring", r"some\nstring"),
            ("some\r\nstring", r"some\r\nstring"),
        ];

        let tf = compact()?;

        cases.into_iter().try_for_each(|(given, want)| -> Result<()> {
            assert_eq!(want, tf(given.to_string())?);
            Ok(())
        })
    }
}