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
4
pub fn begin(public_key: &Key) -> String {
23
4
    let map = HashMap::from([(PUBLIC, public_key.as_ref())]);
24

            
25
4
    encode(&map)
26
}
27

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

            
41
4
    Ok(aes_key)
42
}
43

            
44
// Retrieves the secret from final secret exchange string
45
4
pub fn retrieve(exchange: &str, aes_key: &Key) -> Option<oo7::Secret> {
46
4
    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
8
    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
8
    if secret.is_empty() || secret.len() % 16 != 0 {
55
        return None;
56
    }
57

            
58
4
    let iv = decoded.get(IV)?;
59

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

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

            
78
4
    exchange
79
}
80

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

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

            
99
4
    Some(map)
100
}
101

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            
191
            let decrypted = retrieve(&final_exchange, &peer_2_aes_key);
192

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