CH-J ToolsSSH کے ذریعے سرور کا انتظام
مینو
شائع شدہ سورس

CH-J Tools

ایپلیکیشن کی کسی مخصوص ریلیز کی ڈائریکٹریز اور فائلیں دیکھیں۔

سورس ZIP ڈاؤن لوڈ کریں
CH-J / Server Manager 1704 فائلیں
0.1.2-beta شامل کیا گیا: · بلڈ کیا گیا:
openpgp_data.rs شامل کیا گیا: تاریخ درج نہیں 15.6 KB · 408 لائنیں
کوڈ فائل ڈاؤن لوڈ کریں
1//! Local OpenPGP message operations. No paths, network, vault writes or UI logging.
2use crate::core::{
3 error::{CoreError, CoreResult},
4 openpgp, openpgp_algorithms,
5};
6use pgp::{
7 composed::{
8 CleartextSignedMessage, Deserializable, Message, MessageBuilder, SignedPublicKey,
9 SignedSecretKey,
10 },
11 crypto::{aead::AeadAlgorithm, sym::SymmetricKeyAlgorithm},
12 types::{KeyDetails, Password, SigningKey, VerifyingKey},
13};
14use serde_json::{json, Value};
15use std::io::Read;
16use zeroize::Zeroizing;
18pub const MAX_DATA: u64 = 64 * 1024 * 1024;
19fn error() -> CoreError {
20 CoreError::new(
21 "OPENPGP_OPERATION_FAILED",
22 "OpenPGP operation failed. Check the key, passphrase, message and supported format.",
23 )
25fn unsupported() -> CoreError {
26 CoreError::new(
27 "OPENPGP_PROFILE_UNSUPPORTED",
28 "This message operation requires an RFC 4880/9580/9980 V4 or V6 certificate.",
29 )
31fn bounded(bytes: &[u8]) -> CoreResult<()> {
32 if bytes.len() as u64 > MAX_DATA {
33 Err(CoreError::plain("OpenPGP input exceeds 64 MiB."))
34 } else {
35 Ok(())
36 }
38fn public(entry: &Value) -> CoreResult<(SignedPublicKey, Value)> {
39 let text = entry["public"].as_str().ok_or_else(error)?;
40 if crate::core::openpgp_compat::handles(text.as_bytes())? {
41 return Err(unsupported());
42 }
43 let (key, _) =
44 SignedPublicKey::from_reader_single(std::io::Cursor::new(text)).map_err(|_| error())?;
45 key.verify_bindings().map_err(|_| error())?;
46 if entry["fingerprint"] != hex::encode_upper(key.fingerprint().as_ref()) {
47 return Err(error());
48 }
49 Ok((key, openpgp::describe_public(text)?))
51fn private(entry: &Value) -> CoreResult<(SignedSecretKey, Value)> {
52 let (_, meta) = public(entry)?;
53 let (key, _) = SignedSecretKey::from_reader_single(std::io::Cursor::new(
54 entry["private"].as_str().ok_or_else(error)?,
55 ))
56 .map_err(|_| error())?;
57 key.verify_bindings().map_err(|_| error())?;
58 if entry["fingerprint"] != hex::encode_upper(key.fingerprint().as_ref()) {
59 return Err(error());
60 }
61 Ok((key, meta))
63fn effective(meta: &Value, key: &dyn KeyDetails, cap: char) -> bool {
64 std::iter::once(meta)
65 .chain(meta["subkeys"].as_array().into_iter().flatten())
66 .any(|m| {
67 m["fingerprint"] == hex::encode_upper(key.fingerprint().as_ref())
68 && m["effectiveCapabilities"]
69 .as_str()
70 .unwrap_or("")
71 .contains(cap)
72 })
74fn signing<'a>(key: &'a SignedSecretKey, meta: &Value) -> CoreResult<&'a dyn SigningKey> {
75 for sub in &key.secret_subkeys {
76 if effective(meta, &sub.key, 'S') {
77 return Ok(&sub.key);
78 }
79 }
80 if effective(meta, &key.primary_key, 'S') {
81 Ok(&key.primary_key)
82 } else {
83 Err(error())
84 }
86fn encrypt_format(entry: &Value, data: &[u8], armored: bool) -> CoreResult<Vec<u8>> {
87 bounded(data)?;
88 let (key, meta) = public(entry)?;
89 let selected = key
90 .public_subkeys
91 .iter()
92 .filter(|s| effective(&meta, &s.key, 'E'))
93 .max_by_key(|s| s.key.algorithm().is_pqc());
94 let builder = MessageBuilder::from_reader("", std::io::Cursor::new(data));
95 macro_rules! finish {
96 ($configured:expr) => {{
97 let mut builder = $configured;
98 if let Some(sub) = selected {
99 builder
100 .encrypt_to_key(&mut rand::thread_rng(), &sub.key)
101 .map_err(|_| error())?;
102 } else if effective(&meta, &key.primary_key, 'E') {
103 builder
104 .encrypt_to_key(&mut rand::thread_rng(), &key.primary_key)
105 .map_err(|_| error())?;
106 } else {
107 return Err(error());
108 }
109 if armored {
110 builder
111 .to_armored_string(&mut rand::thread_rng(), Default::default())
112 .map(String::into_bytes)
113 .map_err(|_| error())
114 } else {
115 builder.to_vec(&mut rand::thread_rng()).map_err(|_| error())
116 }
117 }};
118 }
119 if meta["version"] == 6 {
120 finish!(builder.seipd_v2(
121 &mut rand::thread_rng(),
122 SymmetricKeyAlgorithm::AES256,
123 AeadAlgorithm::Ocb,
124 Default::default()
125 ))
126 } else {
127 finish!(builder.seipd_v1(&mut rand::thread_rng(), SymmetricKeyAlgorithm::AES256))
128 }
130pub fn encrypt(entry: &Value, data: &[u8]) -> CoreResult<Vec<u8>> {
131 encrypt_format(entry, data, false)
133pub fn encrypt_armored(entry: &Value, data: &[u8]) -> CoreResult<String> {
134 String::from_utf8(encrypt_format(entry, data, true)?).map_err(|_| error())
136pub fn perform_text(entry: &Value, payload: &Value) -> CoreResult<Value> {
137 let input = payload["text"].as_str().ok_or_else(error)?;
138 if input.len() > 1024 * 1024 {
139 return Err(error());
140 }
141 let password = payload["passphrase"].as_str().unwrap_or("");
142 let text = match payload["operation"].as_str().unwrap_or("") {
143 "signText" => sign_text(entry, input, password)?,
144 "verifyText" => String::from_utf8(verify_inline(entry, input.as_bytes())?.to_vec())
145 .map_err(|_| error())?,
146 "encryptText" => encrypt_armored(entry, input.as_bytes())?,
147 "decryptText" => String::from_utf8(decrypt(entry, input.as_bytes(), password)?.to_vec())
148 .map_err(|_| error())?,
149 _ => return Err(error()),
150 };
151 Ok(json!({"text":text,"verified":payload["operation"]=="verifyText"}))
153fn read_message(mut message: Message<'_>) -> CoreResult<(Message<'_>, Zeroizing<Vec<u8>>)> {
154 // Bound both decompression depth and plaintext size before releasing any bytes.
155 for _ in 0..4 {
156 if !message.is_compressed() {
157 break;
158 }
159 message = message.decompress().map_err(|_| error())?;
160 }
161 if message.is_compressed() {
162 return Err(error());
163 }
164 let mut bytes = Zeroizing::new(Vec::new());
165 message
166 .by_ref()
167 .take(MAX_DATA + 1)
168 .read_to_end(&mut bytes)
169 .map_err(|_| error())?;
170 bounded(&bytes)?;
171 Ok((message, bytes))
173fn parse_message(data: &[u8]) -> CoreResult<Message<'_>> {
174 if data.len() as u64 > 96 * 1024 * 1024 {
175 return Err(error());
176 }
177 if data.starts_with(b"-----BEGIN PGP MESSAGE-----") {
178 Message::from_armor(data)
179 .map(|(m, _)| m)
180 .map_err(|_| error())
181 } else {
182 Message::from_bytes(data).map_err(|_| error())
183 }
185pub fn decrypt(entry: &Value, data: &[u8], password: &str) -> CoreResult<Zeroizing<Vec<u8>>> {
186 let (key, _) = private(entry)?;
187 let message = parse_message(data)?
188 .decrypt(&Password::from(password), &key)
189 .map_err(|_| error())?;
190 let (_, bytes) = read_message(message)?;
191 Ok(bytes)
193pub fn sign_inline(entry: &Value, data: &[u8], password: &str) -> CoreResult<Vec<u8>> {
194 bounded(data)?;
195 let (key, meta) = private(entry)?;
196 let signer = signing(&key, &meta)?;
197 let mut builder = MessageBuilder::from_reader("", std::io::Cursor::new(data));
198 builder.sign(
199 signer,
200 Password::from(password),
201 openpgp_algorithms::signing_hash(signer.public_params()),
202 );
203 builder.to_vec(&mut rand::thread_rng()).map_err(|_| error())
205pub fn sign_text(entry: &Value, text: &str, password: &str) -> CoreResult<String> {
206 bounded(text.as_bytes())?;
207 let (key, meta) = private(entry)?;
208 let signer = signing(&key, &meta)?;
209 // CleartextSignedMessage accepts sized signing keys, so select the same effective key concretely.
210 if let Some(sub) = key
211 .secret_subkeys
212 .iter()
213 .find(|s| effective(&meta, &s.key, 'S'))
214 {
215 CleartextSignedMessage::sign(
216 &mut rand::thread_rng(),
217 text,
218 &sub.key,
219 &Password::from(password),
220 )
221 .and_then(|m| m.to_armored_string(Default::default()))
222 .map_err(|_| error())
223 } else {
224 let _ = signer;
225 CleartextSignedMessage::sign(
226 &mut rand::thread_rng(),
227 text,
228 &key.primary_key,
229 &Password::from(password),
230 )
231 .and_then(|m| m.to_armored_string(Default::default()))
232 .map_err(|_| error())
233 }
235fn verify_keys<'a>(key: &'a SignedPublicKey, meta: &Value) -> Vec<&'a dyn VerifyingKey> {
236 let mut keys: Vec<&dyn VerifyingKey> = Vec::new();
237 if effective(meta, &key.primary_key, 'S') {
238 keys.push(&key.primary_key)
239 }
240 for sub in &key.public_subkeys {
241 if effective(meta, &sub.key, 'S') {
242 keys.push(&sub.key)
243 }
244 }
245 keys
247pub fn verify_inline(entry: &Value, data: &[u8]) -> CoreResult<Zeroizing<Vec<u8>>> {
248 bounded(data)?;
249 let (key, meta) = public(entry)?;
250 if data.starts_with(b"-----BEGIN PGP SIGNED MESSAGE-----") {
251 let (signed, _) = CleartextSignedMessage::from_armor(data).map_err(|_| error())?;
252 let current = |result: pgp::errors::Result<&pgp::packet::Signature>| {
253 result.is_ok_and(|sig| {
254 crate::core::updates::openpgp::signature_current(sig, std::time::SystemTime::now())
255 })
256 };
257 let valid = (effective(&meta, &key.primary_key, 'S')
258 && current(signed.verify(&key.primary_key)))
259 || key
260 .public_subkeys
261 .iter()
262 .any(|sub| effective(&meta, &sub.key, 'S') && current(signed.verify(&sub.key)));
263 if !valid {
264 return Err(error());
265 }
266 Ok(Zeroizing::new(signed.signed_text().into_bytes()))
267 } else {
268 let (message, bytes) = read_message(parse_message(data)?)?;
269 if !verify_keys(&key, &meta).iter().any(|k| {
270 message.verify(*k).is_ok_and(|sig| {
271 crate::core::updates::openpgp::signature_current(sig, std::time::SystemTime::now())
272 })
273 }) {
274 return Err(error());
275 }
276 Ok(bytes)
277 }
279pub fn capabilities(entry: &Value, meta: &Value) -> Value {
280 let supported = [4, 6].contains(&meta["version"].as_u64().unwrap_or(0));
281 let e = meta["certificateCapabilities"]
282 .as_str()
283 .unwrap_or("")
284 .contains('E');
285 let pq_cap = |cap: char| {
286 std::iter::once(meta)
287 .chain(meta["subkeys"].as_array().into_iter().flatten())
288 .any(|key| {
289 key["postQuantum"] == true
290 && key["effectiveCapabilities"]
291 .as_str()
292 .unwrap_or("")
293 .contains(cap)
294 })
295 };
296 let pq = std::iter::once(meta)
297 .chain(meta["subkeys"].as_array().into_iter().flatten())
298 .any(|key| key["postQuantum"] == true);
299 json!({"importSupported":true,"exportPublicSupported":true,"exportPrivateSupported":entry["private"].is_string(),"encryptSupported":supported && e,"decryptSupported":supported && entry["private"].is_string(),"inlineSignSupported":supported && entry["private"].is_string() && meta["certificateCapabilities"].as_str().unwrap_or("").contains('S'),"certificatePostQuantumEncryption":pq_cap('E'),"certificatePostQuantumSignatures":pq_cap('S'),"serializationProfile":if pq{"RFC 9980"}else{"OpenPGP"}})
302#[cfg(test)]
303mod tests {
304 use super::*;
305 use crate::core::keychain;
306 #[test]
307 fn rfc9980_real_operations_preserve_versions_and_keychain_roundtrips() {
308 let temp = tempfile::tempdir().unwrap();
309 let file = temp.path().join("payload");
310 let data = b"Synthetic RFC 9980 payload\n";
311 std::fs::write(&file, data).unwrap();
312 for (index, algorithm) in [
313 "ml-dsa65-ed25519",
314 "ml-dsa87-ed448",
315 "slh-dsa-shake128s",
316 "slh-dsa-shake128f",
317 "slh-dsa-shake256s",
318 ]
319 .iter()
320 .enumerate()
321 {
322 let started = std::time::Instant::now();
323 let (out,entry)=openpgp::generate(&json!({"name":"Synthetic PQ identity","algorithm":algorithm,"passphrase":"synthetic-pq-password","subkeys":[{"role":"encrypt","algorithm":"ml-kem768-x25519"}]})).unwrap();
324 let meta = keychain::describe(&entry);
325 assert_eq!(meta["algorithmId"], 30 + index);
326 assert_eq!(meta["version"], 6);
327 assert_eq!(meta["postQuantum"], true);
328 assert_eq!(meta["certificateCapabilities"], "CSE");
329 let parsed = keychain::parse(
330 entry["private"].as_str().unwrap().as_bytes(),
331 "Imported",
332 "",
333 )
334 .unwrap();
335 assert_eq!(parsed["fingerprint"], entry["fingerprint"]);
336 let sig = keychain::sign(&entry, &file, "synthetic-pq-password").unwrap();
337 keychain::verify(&parsed, &file, sig.as_bytes()).unwrap();
338 let signed = sign_inline(&entry, data, "synthetic-pq-password").unwrap();
339 assert_eq!(verify_inline(&parsed, &signed).unwrap().as_slice(), data);
340 let text = sign_text(
341 &entry,
342 "Synthetic text\nSecond line",
343 "synthetic-pq-password",
344 )
345 .unwrap();
346 assert!(verify_inline(&parsed, text.as_bytes()).is_ok());
347 let encrypted = encrypt(&entry, data).unwrap();
348 assert_eq!(
349 decrypt(&parsed, &encrypted, "synthetic-pq-password")
350 .unwrap()
351 .as_slice(),
352 data
353 );
354 assert!(decrypt(&parsed, &encrypted, "wrong").is_err());
355 let mut corrupted = signed.clone();
356 let last = corrupted.len() - 1;
357 corrupted[last] ^= 1;
358 assert!(verify_inline(&parsed, &corrupted).is_err());
359 let revoked = keychain::revoke(&entry, "synthetic-pq-password").unwrap();
360 assert_eq!(keychain::describe(&revoked)["revoked"], true);
361 assert!(verify_inline(&revoked, &signed).is_err());
362 let imported_rev = keychain::parse(
363 out["files"][2]["content"].as_str().unwrap().as_bytes(),
364 "Revoked",
365 "",
366 )
367 .unwrap();
368 assert_eq!(imported_rev["revoked"], true);
369 eprintln!(
370 "RFC9980 {algorithm} lifecycle passed in {}ms",
371 started.elapsed().as_millis()
372 );
373 }
374 }
375 #[test]
376 fn kem_variants_and_v4_exception_use_real_session_key_encapsulation() {
377 for (version, algorithm) in [
378 (4, "ml-kem768-x25519"),
379 (6, "ml-kem768-x25519"),
380 (6, "ml-kem1024-x448"),
381 ] {
382 let (_,entry)=openpgp::generate(&json!({"name":"Synthetic KEM fixture","version":version,"subkeys":[{"role":"encrypt","algorithm":algorithm}]})).unwrap();
383 let cipher = encrypt(&entry, b"Synthetic KEM session payload").unwrap();
384 assert_eq!(
385 decrypt(&entry, &cipher, "").unwrap().as_slice(),
386 b"Synthetic KEM session payload"
387 );
388 let mut tampered = cipher.clone();
389 let end = tampered.len() - 1;
390 tampered[end] ^= 1;
391 assert!(decrypt(&entry, &tampered, "").is_err());
392 for end in [0, 1, cipher.len() / 2, cipher.len() - 1] {
393 assert!(decrypt(&entry, &cipher[..end], "").is_err());
394 }
395 }
396 for version in [2, 3, 4, 5] {
397 for algorithm in [
398 "ml-dsa65-ed25519",
399 "ml-dsa87-ed448",
400 "slh-dsa-shake128s",
401 "slh-dsa-shake128f",
402 "slh-dsa-shake256s",
403 ] {
404 assert!(openpgp::generate(&json!({"name":"Rejected PQ profile","version":version,"algorithm":algorithm,"legacyConfirmed":true})).is_err());
405 }
406 }
407 }
SHA-256deb3ff9b9da83f5b7a0e5933de05380895bf6c129885e6eb6cd165700a7ef9d6

CH-J Proprietary Software License 1.14 · سورس CH-J Proprietary Software License 1.14 کے تحت فراہم کیا جاتا ہے۔ اس کی دستیابی لائسنس کی شرائط نہیں بدلتی اور نہ ہی اضافی حقوق دیتی ہے۔

آرکائیو کا SHA-256ac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f
فائلیں