1
// SecretExchange: Exchange secrets between processes in an unexposed way.
2

            
3
// Initial C implementation: https://gitlab.gnome.org/GNOME/gcr/-/blob/master/gcr/gcr-secret-exchange.c
4

            
5
// The initial implementation of SecretExchange/GCRSecretExchange uses a KeyFile
6
// to encode/parse the payload. In this implementation the payload is based
7
// on a HashMap.
8
// Before any transit operations the payload is base64 encoded and parsed into a
9
// String.
10

            
11
use std::collections::HashMap;
12

            
13
use base64::prelude::*;
14
use oo7::{Key, crypto};
15

            
16
const SECRET: &str = "secret";
17
const PUBLIC: &str = "public";
18
const IV: &str = "iv";
19
const PROTOCOL: &str = "[sx-aes-1]\n";
20

            
21
// Creates the initial payload containing public_key
22
2
pub fn begin(public_key: &Key) -> String {
23
2
    let map = HashMap::from([(PUBLIC, public_key.as_ref())]);
24

            
25
2
    encode(&map)
26
}
27

            
28
// Creates the shared secret: an AES key
29
2
pub fn handshake(private_key: &Key, exchange: &str) -> Result<Key, crypto::Error> {
30
2
    let decoded =
31
        decode(exchange).expect("SecretExchange decode error: failed to decode exchange string");
32
    let public_key = Key::new(
33
2
        decoded
34
2
            .get(PUBLIC)
35
2
            .expect("SecretExchange decode error: PUBLIC parameter is empty")
36
2
            .to_vec(),
37
    );
38
    // Above two calls should never fail during SecretExchange
39
4
    let aes_key = crate::gnome::crypto::generate_aes_key(private_key, &public_key)?;
40

            
41
2
    Ok(aes_key)
42
}
43

            
44
// Retrieves the secret from final secret exchange string
45
2
pub fn retrieve(exchange: &str, aes_key: &Key) -> Option<oo7::Secret> {
46
2
    let decoded = decode(exchange)?;
47

            
48
    // If we cancel an ongoing prompt call, the final exchange won't have the secret
49
    // or IV. The following is to avoid `Option::unwrap()` on a `None` value
50
4
    let secret = decoded.get(SECRET)?;
51

            
52
    // AES ciphertext must be a multiple of 16 bytes (block size)
53
    // and at least 16 bytes (minimum for PKCS7 padding)
54
4
    if secret.is_empty() || secret.len() % 16 != 0 {
55
        // Invalid ciphertext - return a false secret to avoid decryption errors
56
        let false_secret = vec![0, 1];
57
        return Some(oo7::Secret::from(false_secret));
58
    }
59

            
60
2
    let iv = decoded.get(IV)?;
61

            
62
2
    match crypto::decrypt(secret, aes_key, iv) {
63
2
        Ok(decrypted) => Some(oo7::Secret::from(decrypted)),
64
        Err(err) => {
65
            tracing::error!("Failed to do crypto decrypt: {}", err);
66
            None
67
        }
68
    }
69
}
70

            
71
// Converts a HashMap into a payload String
72
2
fn encode(map: &HashMap<&str, &[u8]>) -> String {
73
2
    let mut exchange = map
74
        .iter()
75
6
        .map(|(key, value)| format!("{}={}", key, BASE64_STANDARD.encode(value)))
76
        .collect::<Vec<_>>()
77
        .join("\n");
78
2
    exchange.insert_str(0, PROTOCOL); // Add PROTOCOL prefix
79

            
80
2
    exchange
81
}
82

            
83
// Converts a payload String into a HashMap
84
2
fn decode(exchange: &str) -> Option<HashMap<&str, Vec<u8>>> {
85
2
    let (_, exchange) = exchange.split_once(PROTOCOL)?; // Remove PROTOCOL prefix
86
2
    let mut map: HashMap<&str, Vec<u8>> = HashMap::new();
87

            
88
6
    for pair in exchange.split('\n') {
89
4
        if pair.is_empty() {
90
            // To avoid splitting an empty line (last new line)
91
            break;
92
        }
93
2
        let (key, value) = pair.split_once("=")?;
94
2
        let encoded = BASE64_STANDARD.decode(value).unwrap_or(vec![]);
95
4
        if encoded.is_empty() {
96
            return None;
97
        }
98
4
        map.insert(key, encoded);
99
    }
100

            
101
2
    Some(map)
102
}
103

            
104
#[cfg(test)]
105
mod test {
106
    use super::*;
107

            
108
    #[test]
109
    fn test_retrieve() {
110
        let exchange = "[sx-aes-1]
111
public=/V6FpknNXlOGJwPqXtN0RaED2bS5JyYbftv7WbD0gWiVTMoNgxkAuOX2g+zUO/4TdfBJ6viPRcNdYV+KcxskGvhYouFXs+IgKqNO0MF0CNnWra1I6G56SM4Bgstkx9M5J+1f83l/BTAxlLsAppeLkqEEVSQoy9jXhPOrl5XlIzF2DvriYh+FInB7SFz4VzE3KVq40p7tA9+iAVQg1o9qkQHLazFb1DfbWRgvhDVhwNkk1fIlepIeM426gdmHIAxP
112
secret=DBeLBvEgGuGygDm+XnkxyQ==
113
iv=8e3N+gx553PgQlfTKRK3JA==";
114

            
115
        let aes_key = Key::new(vec![
116
            204, 53, 139, 40, 55, 167, 183, 240, 191, 252, 186, 174, 28, 36, 229, 26,
117
        ]);
118

            
119
        let decrypted = retrieve(exchange, &aes_key).unwrap();
120
        assert_eq!(b"password".to_vec(), decrypted.to_vec());
121
    }
122

            
123
    #[test]
124
    fn test_secret_exchange() {
125
        let peer_1_private_key = Key::generate_private_key().unwrap();
126
        let peer_1_public_key =
127
            crate::gnome::crypto::generate_public_key(&peer_1_private_key).unwrap();
128
        let peer_1_exchange = begin(&peer_1_public_key);
129

            
130
        let peer_2_private_key = Key::generate_private_key().unwrap();
131
        let peer_2_public_key =
132
            crate::gnome::crypto::generate_public_key(&peer_2_private_key).unwrap();
133
        let peer_2_exchange = begin(&peer_2_public_key);
134

            
135
        let peer_1_aes_key = handshake(&peer_1_private_key, &peer_2_exchange).unwrap();
136
        let peer_2_aes_key = handshake(&peer_2_private_key, &peer_1_exchange).unwrap();
137
        let iv = crypto::generate_iv().unwrap();
138
        let encrypted = crypto::encrypt(b"password", &peer_1_aes_key, &iv).unwrap();
139

            
140
        let map = HashMap::from([
141
            (PUBLIC, peer_1_public_key.as_ref()),
142
            (SECRET, encrypted.as_ref()),
143
            (IV, iv.as_ref()),
144
        ]);
145
        let final_exchange = encode(&map);
146

            
147
        let decrypted = retrieve(&final_exchange, &peer_2_aes_key).unwrap();
148
        assert_eq!(b"password".to_vec(), decrypted.to_vec());
149
    }
150

            
151
    #[test]
152
    fn test_retrieve_with_different_lengths() {
153
        let peer_1_private_key = Key::generate_private_key().unwrap();
154
        let peer_1_public_key =
155
            crate::gnome::crypto::generate_public_key(&peer_1_private_key).unwrap();
156
        let peer_1_exchange = begin(&peer_1_public_key);
157

            
158
        let peer_2_private_key = Key::generate_private_key().unwrap();
159
        let peer_2_public_key =
160
            crate::gnome::crypto::generate_public_key(&peer_2_private_key).unwrap();
161
        let peer_2_exchange = begin(&peer_2_public_key);
162

            
163
        let peer_1_aes_key = handshake(&peer_1_private_key, &peer_2_exchange).unwrap();
164
        let peer_2_aes_key = handshake(&peer_2_private_key, &peer_1_exchange).unwrap();
165

            
166
        let test_cases = vec![
167
            "a",                                                            /* 1 byte -> 16 bytes encrypted */
168
            "short",                     // 5 bytes -> 16 bytes encrypted
169
            "password",                  // 8 bytes -> 16 bytes encrypted
170
            "exactly-16-byte",           // 16 bytes -> 32 bytes encrypted
171
            "test-password-long-enough", // 25 bytes -> 32 bytes encrypted
172
            "this-is-a-very-long-password-that-should-encrypt-to-48-bytes", // 48+ bytes
173
        ];
174

            
175
        for password in test_cases {
176
            let iv = crypto::generate_iv().unwrap();
177
            let encrypted = crypto::encrypt(password.as_bytes(), &peer_1_aes_key, &iv).unwrap();
178

            
179
            // Verify encrypted length is a multiple of 16
180
            assert_eq!(
181
                encrypted.len() % 16,
182
                0,
183
                "Encrypted password should be multiple of 16"
184
            );
185

            
186
            let map = HashMap::from([
187
                (PUBLIC, peer_1_public_key.as_ref()),
188
                (SECRET, encrypted.as_ref()),
189
                (IV, iv.as_ref()),
190
            ]);
191
            let final_exchange = encode(&map);
192

            
193
            let decrypted = retrieve(&final_exchange, &peer_2_aes_key);
194

            
195
            // All valid AES ciphertexts should decrypt successfully
196
            assert!(
197
                decrypted.is_some(),
198
                "Should decrypt password '{}'",
199
                password
200
            );
201
            assert_eq!(
202
                password.as_bytes().to_vec(),
203
                decrypted.unwrap().to_vec(),
204
                "Decrypted password should match original for '{}'",
205
                password
206
            );
207
        }
208
    }
209
}