Skip to main content

kagi_sync/infrastructure/
remote_envelope.rs

1#[cfg(feature = "server")]
2use crate::domain::envelope::SuccessResponse;
3use crate::domain::envelope::{RequestEnvelope, RequestPlaintext};
4use age::{Decryptor, Encryptor, x25519};
5use base64::{Engine as _, engine::general_purpose};
6use kagi_domain::error::DomainError;
7use std::io::{Read, Write};
8use std::str::FromStr;
9
10pub fn encrypt_request(
11    plaintext: &RequestPlaintext,
12    server_recipient: &x25519::Recipient,
13    response_recipient: &x25519::Recipient,
14) -> Result<RequestEnvelope, DomainError> {
15    let mut plaintext_value = serde_json::to_value(plaintext)?;
16    let response_recipient = response_recipient.to_string();
17    let object = plaintext_value.as_object_mut().ok_or_else(|| {
18        DomainError::EncryptFailed("request plaintext must serialize to an object".into())
19    })?;
20    object.insert(
21        "response_recipient".to_string(),
22        serde_json::Value::String(response_recipient.clone()),
23    );
24    let plaintext_json = serde_json::to_vec(&plaintext_value)?;
25    let ciphertext = encrypt_age(&plaintext_json, server_recipient)?;
26    Ok(RequestEnvelope {
27        version: 1,
28        request_id: plaintext.request_id.clone(),
29        server_key_id: "kgs_placeholder".to_string(),
30        response_recipient,
31        ciphertext: general_purpose::STANDARD.encode(&ciphertext),
32    })
33}
34
35#[cfg(feature = "server")]
36pub fn decrypt_request(
37    envelope: &RequestEnvelope,
38    server_identity: &x25519::Identity,
39) -> Result<RequestPlaintext, DomainError> {
40    let ciphertext = general_purpose::STANDARD
41        .decode(&envelope.ciphertext)
42        .map_err(|e| DomainError::DecryptFailed(e.to_string()))?;
43    let plaintext_bytes = decrypt_age(&ciphertext, server_identity)?;
44    let plaintext: RequestPlaintext = serde_json::from_slice(&plaintext_bytes)
45        .map_err(|e| DomainError::DecryptFailed(e.to_string()))?;
46    Ok(plaintext)
47}
48
49#[cfg(feature = "server")]
50pub fn encrypt_response(
51    data: &SuccessResponse,
52    recipient: &x25519::Recipient,
53) -> Result<Vec<u8>, DomainError> {
54    let plaintext = serde_json::to_vec(data)?;
55    encrypt_age(&plaintext, recipient)
56}
57
58pub fn decrypt_response(
59    ciphertext: &[u8],
60    identity: &x25519::Identity,
61) -> Result<serde_json::Value, DomainError> {
62    let plaintext = decrypt_age(ciphertext, identity)?;
63    let value: serde_json::Value = serde_json::from_slice(&plaintext)
64        .map_err(|e| DomainError::DecryptFailed(e.to_string()))?;
65    Ok(value)
66}
67
68pub fn encrypt_bytes(
69    plaintext: &[u8],
70    recipient: &x25519::Recipient,
71) -> Result<Vec<u8>, DomainError> {
72    encrypt_age(plaintext, recipient)
73}
74
75pub fn decrypt_bytes(
76    ciphertext: &[u8],
77    identity: &x25519::Identity,
78) -> Result<Vec<u8>, DomainError> {
79    decrypt_age(ciphertext, identity)
80}
81
82fn encrypt_age(plaintext: &[u8], recipient: &x25519::Recipient) -> Result<Vec<u8>, DomainError> {
83    let encryptor = Encryptor::with_recipients(std::iter::once(recipient as _))
84        .map_err(|e| DomainError::EncryptFailed(e.to_string()))?;
85    let mut encrypted = Vec::new();
86    let mut writer = encryptor
87        .wrap_output(&mut encrypted)
88        .map_err(|e| DomainError::EncryptFailed(e.to_string()))?;
89    writer
90        .write_all(plaintext)
91        .map_err(|e| DomainError::EncryptFailed(e.to_string()))?;
92    writer
93        .finish()
94        .map_err(|e| DomainError::EncryptFailed(e.to_string()))?;
95    Ok(encrypted)
96}
97
98fn decrypt_age(encrypted: &[u8], identity: &x25519::Identity) -> Result<Vec<u8>, DomainError> {
99    let decryptor =
100        Decryptor::new(encrypted).map_err(|e| DomainError::DecryptFailed(e.to_string()))?;
101    let mut reader = decryptor
102        .decrypt(std::iter::once(identity as &dyn age::Identity))
103        .map_err(|e| DomainError::DecryptFailed(e.to_string()))?;
104    let mut decrypted = Vec::new();
105    reader
106        .read_to_end(&mut decrypted)
107        .map_err(|e| DomainError::DecryptFailed(e.to_string()))?;
108    Ok(decrypted)
109}
110
111pub fn parse_recipient(s: &str) -> Result<x25519::Recipient, DomainError> {
112    x25519::Recipient::from_str(s)
113        .map_err(|e| DomainError::RemoteProtocolError(format!("invalid recipient: {e}")))
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119    #[cfg(feature = "server")]
120    use crate::domain::envelope::RequestPlaintext;
121    use age::x25519;
122
123    #[cfg(feature = "server")]
124    fn test_identities() -> (x25519::Identity, x25519::Recipient) {
125        let identity = x25519::Identity::generate();
126        let recipient = identity.to_public();
127        (identity, recipient)
128    }
129
130    #[test]
131    #[cfg(feature = "server")]
132    fn test_encrypt_decrypt_request_roundtrip() {
133        let (server_identity, server_recipient) = test_identities();
134        let (_client_identity, client_recipient) = test_identities();
135
136        let plaintext = RequestPlaintext {
137            version: 1,
138            request_id: "kgr_test".into(),
139            issued_at: "2026-01-01T00:00:00Z".into(),
140            operation: "push".into(),
141            method: "POST".into(),
142            path: "/v1/projects/kgp_test/push".into(),
143            project_id: Some("kgp_test".into()),
144            token: Some("test_token".into()),
145            claim_secret: None,
146            payload: serde_json::json!({"base_revision": 0}),
147        };
148
149        let envelope = encrypt_request(&plaintext, &server_recipient, &client_recipient).unwrap();
150        assert_eq!(envelope.request_id, "kgr_test");
151        assert_eq!(envelope.server_key_id, "kgs_placeholder");
152        assert!(!envelope.ciphertext.is_empty());
153
154        let decrypted = decrypt_request(&envelope, &server_identity).unwrap();
155        assert_eq!(decrypted.request_id, plaintext.request_id);
156        assert_eq!(decrypted.path, plaintext.path);
157        assert_eq!(decrypted.method, plaintext.method);
158    }
159
160    #[test]
161    #[cfg(feature = "server")]
162    fn test_encrypt_decrypt_response_roundtrip() {
163        let (client_identity, client_recipient) = test_identities();
164
165        let response = SuccessResponse {
166            ok: true,
167            request_id: "kgr_test".into(),
168            data: serde_json::json!({"revision": 42}),
169        };
170
171        let ciphertext = encrypt_response(&response, &client_recipient).unwrap();
172        assert!(!ciphertext.is_empty());
173
174        let decrypted = decrypt_response(&ciphertext, &client_identity).unwrap();
175        assert_eq!(decrypted["ok"], true);
176        assert_eq!(decrypted["request_id"], "kgr_test");
177        assert_eq!(decrypted["data"]["revision"], 42);
178    }
179
180    #[test]
181    #[cfg(feature = "server")]
182    fn test_decrypt_with_wrong_identity_fails() {
183        let (server_identity, server_recipient) = test_identities();
184        let (_wrong_identity, wrong_recipient) = test_identities();
185
186        let plaintext = RequestPlaintext {
187            version: 1,
188            request_id: "kgr_test".into(),
189            issued_at: "2026-01-01T00:00:00Z".into(),
190            operation: "test".into(),
191            method: "POST".into(),
192            path: "/v1/test".into(),
193            project_id: None,
194            token: None,
195            claim_secret: None,
196            payload: serde_json::json!({}),
197        };
198
199        let envelope = encrypt_request(&plaintext, &server_recipient, &wrong_recipient).unwrap();
200        assert!(decrypt_request(&envelope, &server_identity).is_ok());
201
202        // But encrypting for wrong recipient and decrypting with wrong identity should fail
203        let (_other_identity, other_recipient) = test_identities();
204        let envelope2 = encrypt_request(&plaintext, &other_recipient, &wrong_recipient).unwrap();
205        assert!(decrypt_request(&envelope2, &server_identity).is_err());
206    }
207
208    #[test]
209    fn test_parse_recipient_valid() {
210        let identity = x25519::Identity::generate();
211        let recipient = identity.to_public().to_string();
212        assert!(parse_recipient(&recipient).is_ok());
213    }
214
215    #[test]
216    fn test_parse_recipient_invalid() {
217        assert!(parse_recipient("not_a_recipient").is_err());
218    }
219}