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
49
    // secret or IV. The following is to avoid `Option::unwrap()` on a
50
    // `None` value
51
8
    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
8
    if secret.is_empty() || secret.len() % 16 != 0 {
56
        return None;
57
    }
58

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

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

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

            
79
4
    exchange
80
}
81

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

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

            
100
4
    Some(map)
101
}
102

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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