mops/
lib.rs

1use std::{format, sync::Arc};
2
3use aes_gcm::{AeadInPlace, KeyInit};
4use azure_identity::DefaultAzureCredentialBuilder;
5use azure_security_keyvault::prelude::{DecryptParameters, EncryptionAlgorithm};
6use serde::Deserialize;
7
8use base64::Engine;
9use typenum::U32;
10
11pub type SopsAES = aes_gcm::AesGcm<aes::Aes256, typenum::U32>;
12type UnsupportedVault = Vec<serde_json::Map<String, serde_json::Value>>;
13
14#[derive(Debug, Deserialize)]
15pub struct SopsFile {
16    sops: Sops,
17    #[serde(flatten)]
18    pub content: serde_json::Map<String, serde_json::Value>,
19}
20
21impl SopsFile {
22    pub async fn get_ciphers(&self) -> Vec<SopsAES> {
23        let mut ciphers: Vec<SopsAES> = Vec::new();
24
25        // Check for Azure Key Vault backed keys
26        if let Some(ref vec_akvs) = self.sops.azure_kv {
27            let creds = Arc::new(
28                DefaultAzureCredentialBuilder::new()
29                    .exclude_managed_identity_credential()
30                    .build(),
31            );
32
33            for akv in vec_akvs {
34                let enc_key = base64::engine::general_purpose::URL_SAFE_NO_PAD
35                    .decode(akv.enc.clone())
36                    .expect("failed to debase64 encrypted master key");
37
38                let key_client =
39                    azure_security_keyvault::KeyClient::new(&akv.vault_url, creds.clone())
40                        .expect("failed to create a Key Client");
41
42                let params = DecryptParameters {
43                    ciphertext: enc_key,
44                    decrypt_parameters_encryption:
45                        azure_security_keyvault::prelude::DecryptParametersEncryption::Rsa(
46                            azure_security_keyvault::prelude::RsaDecryptParameters {
47                                algorithm: EncryptionAlgorithm::RsaOaep256,
48                            },
49                        ),
50                };
51
52                let master_key = key_client
53                    .decrypt(akv.name.clone(), params)
54                    .await
55                    .expect("master key decryption failed")
56                    .result;
57
58                let cipher =
59                    SopsAES::new_from_slice(&master_key).expect("failed to construct a cipher");
60
61                ciphers.push(cipher);
62            }
63        }
64
65        ciphers
66    }
67
68    pub fn get_content(&self, key: &str) -> EncryptedContent {
69        let enc_stuff = self
70            .content
71            .get(key)
72            .expect("failed to find the key from the file")
73            .as_str()
74            .expect("the value wasn't string");
75
76        let line_regex = regex::Regex::new(r"^ENC\[AES256_GCM,data:(?P<data>(.+)),iv:(?P<iv>(.+)),tag:(?P<tag>(.+)),type:(?P<type>(.+))\]").expect("could not compile regex");
77        let captures = line_regex.captures(enc_stuff).expect("bad format");
78
79        let data = base64::engine::general_purpose::STANDARD
80            .decode(&captures["data"])
81            .expect("failed to debase64 data");
82        let iv = base64::engine::general_purpose::STANDARD
83            .decode(&captures["iv"])
84            .expect("failed to debase64 iv");
85        let tag_raw = base64::engine::general_purpose::STANDARD
86            .decode(&captures["tag"])
87            .expect("failed to debase64 tag");
88
89        let nonce = aes_gcm::Nonce::from_slice(&iv);
90        let tag = aes_gcm::Tag::from_slice(&tag_raw);
91
92        EncryptedContent {
93            data,
94            nonce: *nonce,
95            tag: *tag,
96            path: key.to_string(),
97        }
98    }
99}
100
101pub struct EncryptedContent {
102    data: Vec<u8>,
103    nonce: aes_gcm::Nonce<U32>,
104    tag: aes_gcm::Tag,
105    path: String,
106}
107
108impl EncryptedContent {
109    pub fn decrypt(&self, ciphers: &[SopsAES]) -> String {
110        ciphers
111            .iter()
112            .find_map(|cipher| {
113                let mut buffer = self.data.clone();
114                match cipher.decrypt_in_place_detached(
115                    &self.nonce,
116                    format!("{}:", self.path).as_bytes(),
117                    &mut buffer,
118                    &self.tag,
119                ) {
120                    Ok(_) => Some(String::from_utf8(buffer).expect("not utf-8 enough")),
121                    Err(_) => None,
122                }
123            })
124            .expect("no keys found")
125    }
126}
127
128#[derive(Debug, Deserialize)]
129pub struct Sops {
130    kms: Option<UnsupportedVault>, // won't actually be missing but instead filled with a null
131    gcp_kms: Option<UnsupportedVault>,
132    azure_kv: Option<Vec<AzureKV>>,
133    hc_vault: Option<UnsupportedVault>,
134    age: Option<UnsupportedVault>,
135    lastmodified: String, // a timestamp
136    mac: String,
137    pgp: Option<UnsupportedVault>,
138    unencrypted_suffix: String,
139    version: String,
140}
141
142#[derive(Debug, Deserialize)]
143pub struct AzureKV {
144    vault_url: String,
145    name: String,
146    version: String,
147    created_at: String,
148    enc: String,
149}