Sumber yang diterbitkan
Muat turun ZIP sumber
CH-J Tools
Layari direktori dan fail untuk keluaran aplikasi tertentu.
CH-J / Server Manager
1704 fail
0.1.2-beta
Ditambah: · Dibina:
1
//! Local OpenPGP message operations. No paths, network, vault writes or UI logging.2
use crate::core::{3
error::{CoreError, CoreResult},4
openpgp, openpgp_algorithms,5
};6
use 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
};14
use serde_json::{json, Value};15
use std::io::Read;16
use zeroize::Zeroizing;18
pub const MAX_DATA: u64 = 64 * 1024 * 1024;19
fn error() -> CoreError {20
CoreError::new(21
"OPENPGP_OPERATION_FAILED",22
"OpenPGP operation failed. Check the key, passphrase, message and supported format.",23
)24
}25
fn unsupported() -> CoreError {26
CoreError::new(27
"OPENPGP_PROFILE_UNSUPPORTED",28
"This message operation requires an RFC 4880/9580/9980 V4 or V6 certificate.",29
)30
}31
fn 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
}37
}38
fn 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)?))50
}51
fn 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))62
}63
fn 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
})73
}74
fn 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
}85
}86
fn encrypt_format(entry: &Value, data: &[u8], armored: bool) -> CoreResult<Vec<u8>> {87
bounded(data)?;88
let (key, meta) = public(entry)?;89
let selected = key90
.public_subkeys91
.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
builder100
.encrypt_to_key(&mut rand::thread_rng(), &sub.key)101
.map_err(|_| error())?;102
} else if effective(&meta, &key.primary_key, 'E') {103
builder104
.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
builder111
.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
}129
}130
pub fn encrypt(entry: &Value, data: &[u8]) -> CoreResult<Vec<u8>> {131
encrypt_format(entry, data, false)132
}133
pub fn encrypt_armored(entry: &Value, data: &[u8]) -> CoreResult<String> {134
String::from_utf8(encrypt_format(entry, data, true)?).map_err(|_| error())135
}136
pub 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"}))152
}153
fn 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
message166
.by_ref()167
.take(MAX_DATA + 1)168
.read_to_end(&mut bytes)169
.map_err(|_| error())?;170
bounded(&bytes)?;171
Ok((message, bytes))172
}173
fn 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
}184
}185
pub 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)192
}193
pub 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())204
}205
pub 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) = key211
.secret_subkeys212
.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
}234
}235
fn 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
keys246
}247
pub 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
|| key260
.public_subkeys261
.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
}278
}279
pub 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"] == true290
&& 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"}})300
}302
#[cfg(test)]303
mod 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
data353
);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
}408
}SHA-256
deb3ff9b9da83f5b7a0e5933de05380895bf6c129885e6eb6cd165700a7ef9d6CH-J Proprietary Software License 1.14 · Kod sumber disediakan di bawah CH-J Proprietary Software License 1.14. Ketersediaannya tidak mengubah terma lesen atau memberikan hak tambahan.
SHA-256 arkib
ac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f