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
const CIPHER_TEXT_LEN: usize = 16;
21

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

            
26
2
    encode(&map)
27
}
28

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

            
42
2
    Ok(aes_key)
43
}
44

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

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

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

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

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

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

            
81
2
    exchange
82
}
83

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

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

            
102
2
    Some(map)
103
}
104

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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