- Introducers can now be created by specifying a file. The certificate gets imported and declared as an introducer for the passed domains. - Add some checks, so that the imported certificate can actually be used as an introducer (certificate is alive, not revoked, has certification capabilities)
569 lines
19 KiB
Rust
569 lines
19 KiB
Rust
//! cryptographic functions
|
|
|
|
use std::io::Write;
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
use std::collections::HashMap;
|
|
use std::collections::hash_map::Entry::{Occupied, Vacant};
|
|
|
|
use anyhow;
|
|
use sequoia_openpgp::cert::ValidCert;
|
|
use tokio::task::JoinSet;
|
|
use bytes::Bytes;
|
|
use sequoia_net::{KeyServer, wkd, dane};
|
|
use sequoia_openpgp::packet::prelude::SignatureBuilder;
|
|
use sequoia_openpgp::parse::{PacketParser, PacketParserResult, Parse};
|
|
use sequoia_openpgp::policy::StandardPolicy;
|
|
use sequoia_openpgp::serialize::stream::{Message, Armorer, Encryptor, LiteralWriter};
|
|
use sequoia_openpgp::{Fingerprint, Cert, Packet};
|
|
use sequoia_openpgp::cert::raw::RawCertParser;
|
|
use sequoia_openpgp::types::SignatureType;
|
|
use sequoia_cert_store::{Store, CertStore, LazyCert, StoreUpdate};
|
|
use sequoia_cert_store::store::MergePublicCollectStats;
|
|
use sequoia_wot::{self as wot};
|
|
use wot::store::Store as _;
|
|
use wot::{Depth, Path};
|
|
|
|
use crate::types::errors::HuskError;
|
|
use crate::types::husk_context::HuskContext;
|
|
use crate::types::introducer::Introducer;
|
|
use crate::types::recipient::Recipient;
|
|
use crate::mail;
|
|
use crate::common::escape;
|
|
|
|
/// Returns the local trust root from the cert_store. If the trust root
|
|
/// cannot be found, it returns an error.
|
|
pub fn get_local_trust_root(cert_store: &CertStore) -> anyhow::Result<Cert> {
|
|
|
|
let root = cert_store.certd()
|
|
.and_then(|certd| {
|
|
match certd.certd().get(sequoia_cert_store::store::openpgp_cert_d::TRUST_ROOT) {
|
|
Ok(Some((_tag, bytes))) => Some(bytes),
|
|
Ok(None) => None,
|
|
Err(_) => None
|
|
}
|
|
})
|
|
.and_then(|bytes| {
|
|
match RawCertParser::from_bytes(&bytes[..]) {
|
|
Ok(mut parser) => {
|
|
match parser.next() {
|
|
Some(Ok(cert)) => {
|
|
Cert::from_bytes(cert.as_bytes()).ok()
|
|
},
|
|
Some(Err(_))
|
|
| None => None
|
|
}
|
|
},
|
|
Err(_) => None,
|
|
}
|
|
});
|
|
|
|
match root {
|
|
Some(r) => Ok(r),
|
|
None => Err(HuskError::NoLocalTrustRoot.into())
|
|
}
|
|
|
|
}
|
|
|
|
/// Returns all local certificates with an authenticated binding to user id 'email'.
|
|
/// The certificates are not checked for usefulness (for instance, they might
|
|
/// miss an encryption subkey).
|
|
pub fn get_local_certificates<'hc>(context: &HuskContext<'hc>, email: &str)
|
|
-> Vec<Arc<LazyCert<'hc>>> {
|
|
|
|
log::debug!("local lookup for {}", email);
|
|
|
|
let cert_store = &context.cert_store;
|
|
let trust_roots = vec![(context.local_trust_root.fingerprint(), wot::FULLY_TRUSTED)];
|
|
let wot_store = wot::store::CertStore::from_store(cert_store, &context.policy, None);
|
|
let nb = wot::NetworkBuilder::rooted(&wot_store, &*trust_roots);
|
|
let n = nb.build();
|
|
|
|
let bindings: Vec<_> = n.lookup_synopses_by_email(email)
|
|
.into_iter()
|
|
.map(|(fp, userid)| (fp, Some(userid)))
|
|
.collect();
|
|
|
|
let mut result = vec![];
|
|
for (fpr, some_userid) in bindings {
|
|
if let Some(userid) = some_userid {
|
|
let paths = n.authenticate(userid, &fpr, wot::FULLY_TRUSTED);
|
|
for (path, _) in paths.iter() {
|
|
if path.amount() >= wot::FULLY_TRUSTED {
|
|
log::debug!("{} authenticated!!!", email);
|
|
if let Ok(cert) = cert_store.lookup_by_cert_fpr(&path.target().fingerprint()) {
|
|
result.push(cert);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
result
|
|
}
|
|
|
|
|
|
const USER_AGENT: &'static str = concat!("husk/", env!("CARGO_PKG_VERSION"));
|
|
const CONNECT_TIMEOUT: Duration = Duration::new(5, 0);
|
|
const REQUEST_TIMEOUT: Duration = Duration::new(5, 0);
|
|
|
|
/// Try to fetch certificates for `email` from online sources (keyservers, wkd,
|
|
/// dane).
|
|
pub async fn lookup_certificates<'hc>(context: &HuskContext<'hc>, email: &str)
|
|
-> anyhow::Result<Vec<Cert>> {
|
|
|
|
log::debug!("remote lookup for {}", email);
|
|
|
|
let mut jobs = JoinSet::new();
|
|
let http_client = sequoia_net::reqwest::Client::builder()
|
|
.user_agent(USER_AGENT)
|
|
.connect_timeout(CONNECT_TIMEOUT)
|
|
.timeout(REQUEST_TIMEOUT)
|
|
.build()?;
|
|
|
|
// local function to check certificates for usability
|
|
let collect_certs = |certs: Vec<Result<Cert, anyhow::Error>>, email: String, policy: &StandardPolicy| {
|
|
let mut result = Vec::new();
|
|
|
|
for cert in certs.into_iter().flatten() {
|
|
if let Ok(vc) = cert.with_policy(policy, None) {
|
|
|
|
// check if the returned certificate contains a userid with the
|
|
// email address in question.
|
|
let mut userid_found = false;
|
|
for userid in vc.userids() {
|
|
if let Ok(Some(u)) = userid.userid().email() {
|
|
if u == email {
|
|
userid_found = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if userid_found {
|
|
// check if the certificate can be used (for encryption).
|
|
vc.keys()
|
|
.supported()
|
|
.alive()
|
|
.revoked(false)
|
|
.for_transport_encryption()
|
|
.for_each(|_| {
|
|
// XXX: cert gets returned for each transport key
|
|
// once would be enough
|
|
result.push(cert.clone());
|
|
});
|
|
}
|
|
}
|
|
}
|
|
result
|
|
};
|
|
|
|
// Keyservers
|
|
for keyserver in &context.keyservers {
|
|
|
|
log::debug!("querying {}", keyserver);
|
|
|
|
if let Ok(ks) = KeyServer::with_client(keyserver.as_str(), http_client.clone()) {
|
|
|
|
let e = email.to_string();
|
|
let p = StandardPolicy::new();
|
|
|
|
jobs.spawn(async move {
|
|
let mut partial_result = Vec::new();
|
|
|
|
if let Ok(certs) = ks.search(e.as_str()).await {
|
|
partial_result = collect_certs(certs, e, &p);
|
|
}
|
|
partial_result
|
|
});
|
|
}
|
|
}
|
|
|
|
// WKD
|
|
let e = email.to_string();
|
|
let p = StandardPolicy::new();
|
|
jobs.spawn(async move {
|
|
let mut partial_result = Vec::new();
|
|
|
|
if let Ok(certs) = wkd::get(&http_client.clone(), e.as_str()).await {
|
|
partial_result = collect_certs(certs, e, &p);
|
|
}
|
|
partial_result
|
|
});
|
|
|
|
// DANE
|
|
let e = email.to_string();
|
|
let p = StandardPolicy::new();
|
|
jobs.spawn(async move {
|
|
let mut partial_result = Vec::new();
|
|
|
|
if let Ok(certs) = dane::get(e.as_str()).await {
|
|
partial_result = collect_certs(certs, e, &p);
|
|
}
|
|
partial_result
|
|
});
|
|
|
|
// join the partial results. As the same certificate might be returned by
|
|
// several sources, dedup and merge the certificates.
|
|
let mut set: HashMap<Fingerprint, Cert> = HashMap::new();
|
|
let results = jobs.join_all().await;
|
|
results.into_iter().flatten()
|
|
.for_each(|c| {
|
|
let fpr = c.fingerprint();
|
|
|
|
log::debug!("found cert {}", fpr);
|
|
|
|
match set.entry(fpr.clone()) {
|
|
Occupied(entry) => {
|
|
let existing_cert = entry.into_mut();
|
|
if let Ok(merged_cert) = existing_cert.clone().merge_public(c) {
|
|
*existing_cert = merged_cert;
|
|
}
|
|
},
|
|
Vacant(_entry) => {
|
|
set.insert(fpr.clone(), c);
|
|
},
|
|
}
|
|
});
|
|
|
|
let result = set.into_values().collect();
|
|
Ok(result)
|
|
|
|
}
|
|
|
|
|
|
pub async fn get_certificates<'hc>(context: &HuskContext<'hc>, email: &str)
|
|
-> Vec<Arc<LazyCert<'hc>>> {
|
|
|
|
log::debug!("get certificate for {}", email);
|
|
|
|
let locals = get_local_certificates(context, email);
|
|
if !locals.is_empty() {
|
|
return locals;
|
|
}
|
|
|
|
let mut result = Vec::new();
|
|
|
|
for introducer in &context.introducers {
|
|
log::debug!("consider introducer {:?}",
|
|
introducer.cert.userids().map(|u| u.userid()).collect::<Vec<_>>());
|
|
if introducer.can_introduce(format!("<{}>", email).as_str()) {
|
|
log::debug!(" introducer regex matches!");
|
|
match lookup_certificates(context, email).await {
|
|
Ok(certs) => {
|
|
for cert in certs {
|
|
if introducer.is_introducing(&cert) {
|
|
let lazy_cert = Arc::new(LazyCert::from_cert(cert));
|
|
if context.cert_store.update(lazy_cert.clone()).is_ok() {
|
|
result.push(lazy_cert);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
Err(e) => {
|
|
log::error!("{}", HuskError::KeyLookupError(email.to_string(), e.to_string()));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
result
|
|
}
|
|
|
|
/// Returns all introducers available in cert_store.
|
|
pub fn get_introducers<'c>(cert_store: &CertStore<'c>, policy: &StandardPolicy, local_trust_root: Fingerprint) -> Vec<Introducer> {
|
|
|
|
let mut result = Vec::new();
|
|
|
|
let trust_roots = vec![(local_trust_root, wot::FULLY_TRUSTED)];
|
|
let wot_store = wot::store::CertStore::from_store(cert_store, policy, None);
|
|
let n = wot::NetworkBuilder::rooted(&wot_store, &*trust_roots)
|
|
.build();
|
|
|
|
n.certified_userids().iter().for_each(|(fpr, user_id)| {
|
|
let paths = n.authenticate(user_id, fpr, wot::FULLY_TRUSTED);
|
|
|
|
// find the longest path (highest trust depth)
|
|
let mut depth = Depth::Limit(0);
|
|
let mut target: Option<&Path> = None;
|
|
paths.iter().for_each(|(path, amount)| {
|
|
if *amount == wot::FULLY_TRUSTED {
|
|
let d = path.residual_depth();
|
|
if d > depth {
|
|
depth = d;
|
|
target = Some(path);
|
|
}
|
|
}
|
|
});
|
|
|
|
if depth > Depth::Limit(0) {
|
|
if let Some(p) = target {
|
|
if let Some(c) = p.certifications().last() {
|
|
result.push( Introducer {
|
|
cert: p.target().clone(),
|
|
certification: c.clone(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
result
|
|
}
|
|
|
|
/// Checks if a message contains the OpenPGP artifacts of an encrypted message.
|
|
pub fn is_encrypted(body: &Bytes) -> bool {
|
|
|
|
let mut ppr = match PacketParser::from_bytes(body) {
|
|
Ok(pp) => pp,
|
|
Err(_) => return false, // no OpenPGP artifacts
|
|
};
|
|
|
|
let mut encrypted: bool = false;
|
|
|
|
while let PacketParserResult::Some(pp) = ppr {
|
|
if let Packet::SEIP(_) = pp.packet {
|
|
encrypted = true;
|
|
}
|
|
|
|
// parse the whole message, don't stop at first SEIP
|
|
ppr = match pp.recurse() {
|
|
Ok((_, next_ppr)) => next_ppr,
|
|
Err(_) => return false, // broken artifact -> not encrypted
|
|
}
|
|
}
|
|
if let PacketParserResult::EOF(eof) = ppr {
|
|
if eof.is_message().is_ok() && encrypted {
|
|
return true;
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
/// Encrypt a message for `recipients`.
|
|
pub fn encrypt(context: &HuskContext<'_>, body: &Bytes, recipients: &Vec<Recipient>)
|
|
-> anyhow::Result<Bytes> {
|
|
|
|
let mut keys = Vec::new();
|
|
|
|
for rcpt in recipients {
|
|
if rcpt.can_encrypt() {
|
|
for cert in &rcpt.certs {
|
|
cert.with_policy(&context.policy, None)?
|
|
.keys()
|
|
.supported()
|
|
.alive()
|
|
.revoked(false)
|
|
.for_transport_encryption()
|
|
.for_each(|k| {
|
|
keys.push(k);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
if !keys.is_empty() {
|
|
let mut sink = Vec::new();
|
|
let message = Message::new(&mut sink);
|
|
let message = Armorer::new(message).build()?;
|
|
let message = Encryptor::for_recipients(message, keys).build()?;
|
|
let mut message = LiteralWriter::new(message).build()?;
|
|
message.write_all(&body[..])?;
|
|
message.finalize()?;
|
|
|
|
Ok(Bytes::from(sink))
|
|
} else {
|
|
Err(HuskError::NoEncryptionKeys.into())
|
|
}
|
|
}
|
|
|
|
/// Authenticate a certificate.
|
|
///
|
|
/// If `activate` is true, the certificate becomes an introducer for
|
|
/// `domains`. Passing `None` or an empty list in this case is an error.
|
|
/// If `activate` is false, create a certifiaction with minimal trust_roots
|
|
/// amount and no depth. In this case `domains` is ignored.
|
|
pub fn authenticate(context: &HuskContext, cert: &ValidCert, activate: bool, domains: Option<Vec<String>>)
|
|
-> anyhow::Result<()> {
|
|
|
|
let policy = &context.policy;
|
|
let root_signing_key = match context.local_trust_root.with_policy(policy, None)?
|
|
.keys()
|
|
.supported()
|
|
.alive()
|
|
.revoked(false)
|
|
.for_certification()
|
|
.next() {
|
|
Some(k) => k.key(),
|
|
None => {
|
|
return Err(HuskError::DamangedLocalTrustRoot(
|
|
"missing certification capability".to_string())
|
|
.into());
|
|
}
|
|
};
|
|
let mut root_signer = root_signing_key
|
|
.clone()
|
|
.parts_into_secret()?
|
|
.into_keypair()?;
|
|
|
|
// Create the SignatureBuilder with a default preconfiguration.
|
|
let mut builder =
|
|
SignatureBuilder::new(SignatureType::GenericCertification)
|
|
.set_signature_creation_time(std::time::SystemTime::now())?
|
|
.set_exportable_certification(false)?
|
|
.set_revocable(true)?;
|
|
// set no expiration date
|
|
|
|
// Add domains
|
|
if activate {
|
|
// set the trust level and depth for an active introducer.
|
|
builder = builder.set_trust_signature(1, 120)?;
|
|
|
|
if let Some(domains) = domains {
|
|
for domain in domains {
|
|
if !mail::is_domain(domain.as_str()) {
|
|
return Err(HuskError::InvalidDomainName(domain).into());
|
|
}
|
|
|
|
builder = builder.add_regular_expression(format!("<[^>]+[@.]{}>$",
|
|
escape::regex_ctrl_escape(domain.as_str())?
|
|
))?;
|
|
}
|
|
} else {
|
|
// An introducer without domains would be unlimited.
|
|
return Err(HuskError::UnlimitedIntroducer.into());
|
|
}
|
|
} else {
|
|
// deactive the introducer
|
|
builder = builder.set_trust_signature(0, 1)?;
|
|
}
|
|
|
|
// For a certification, user ids are needed. Build a list of
|
|
// valid (non self revoked) user ids.
|
|
let user_ids: Vec<_> = cert.userids()
|
|
.filter(|u| {
|
|
u.self_revocations().count() == 0
|
|
})
|
|
.collect();
|
|
|
|
if user_ids.is_empty() {
|
|
return Err(HuskError::NoUserIDs(cert.fingerprint().to_hex()).into());
|
|
}
|
|
|
|
// now certify
|
|
let signatures: Vec<_> = user_ids.into_iter()
|
|
.filter_map(|u| {
|
|
let b = builder.clone();
|
|
|
|
b.sign_userid_binding(
|
|
&mut root_signer,
|
|
cert.primary_key().key(),
|
|
u.userid()).ok()
|
|
})
|
|
.collect();
|
|
let (new_cert, _) = cert.cert().clone().insert_packets(signatures)?;
|
|
|
|
// and (re)import
|
|
let new_cert = Arc::new(new_cert.into());
|
|
let stats = MergePublicCollectStats::new();
|
|
let cert_store = &context.cert_store;
|
|
cert_store.update_by(new_cert, &stats)?;
|
|
|
|
log::debug!("{:?}", stats);
|
|
|
|
// XXX: do something with the created stats
|
|
Ok(())
|
|
}
|
|
|
|
pub fn has_certification_capability(cert: &ValidCert) -> bool {
|
|
cert.keys()
|
|
.for_certification()
|
|
.alive()
|
|
.supported()
|
|
.revoked(false)
|
|
.count() > 0
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub mod tests {
|
|
|
|
#[test]
|
|
pub fn test_is_encrypted() {
|
|
|
|
use super::*;
|
|
|
|
// cleartext
|
|
let body = b"Hello world";
|
|
let body = Bytes::from(&body[..]);
|
|
assert!(! is_encrypted(&body));
|
|
|
|
// a valid encrypted message
|
|
let body = b"-----BEGIN PGP MESSAGE-----
|
|
|
|
wWwGFQRhclhA/Qg3CiL81NAswyOWkyk8jxIBB0AmlBkVEVbQst60C/1+mcmIxLbG
|
|
m0w+qv2aP5NQhAetCzDgA/KEhtim0Ebzf06jJG7bITPkYmfN2mizdChgv1f3y2eQ
|
|
80hYpgbD930RtKXTD1TSWgIJAgai2MelZrDAQXIaFKlRRY9u7R1YIXQmMa6uDced
|
|
W/g9mJDd0ral30VL0FfuKRR2Je2TRfizqMb4tL4EIRFqrfSBesb9WcniEfBekG3B
|
|
jk6lh01Ej9QTGQ==
|
|
-----END PGP MESSAGE-----
|
|
";
|
|
|
|
let body = Bytes::from(&body[..]);
|
|
assert!(is_encrypted(&body));
|
|
|
|
// a PUBLIC KEY BLOCK is an artifact, but it's not encrypted
|
|
let body = b"-----BEGIN PGP PUBLIC KEY BLOCK-----
|
|
|
|
xjMEaUE/aRYJKwYBBAHaRw8BAQdAXScnsTjHmy1yZ/SmddPq7tmY4SZwsyWmRNei
|
|
EX60UJDCwBEEHxYKAIMFgmlBP2kFiQWkj70DCwkHCRC9/0MioyzN/0cUAAAAAAAe
|
|
ACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmfUHGvbmmp7vFwpMzGj36fZ
|
|
Px+taozYHxyGljxsJnlV3AMVCggCmwECHgkWIQSQO2vdjfwfK4J6FaK9/0MioyzN
|
|
/wAATokBAP9abr1BeV2qrvwF9TsQDlvgnaNPtzhPy00oDi17/8x5AP99DLK8hAJ3
|
|
FtejeAJ0/2+BfUZzRlF8tlSGe5/Iln9DCM0TPGFsaWNlQGV4YW1wbGUuY29tPsLA
|
|
FAQTFgoAhgWCaUE/aQWJBaSPvQMLCQcJEL3/QyKjLM3/RxQAAAAAAB4AIHNhbHRA
|
|
bm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZyWqUJWT+0+sGq1ci9IkqHvnsUN+c5LR
|
|
B921ZbGlCwLXAxUKCAKZAQKbAQIeCRYhBJA7a92N/B8rgnoVor3/QyKjLM3/AAB5
|
|
+AEAvMCVVynlwsaFQvACga9NTfyZGQ7IjhFhjbCXY+CZbHoA/0FaajzhUF6mKQH7
|
|
RMwMK6Dp0YD2A34/DGb5KbDkLjIOzjMEaUE/aRYJKwYBBAHaRw8BAQdAd4Y3kt2s
|
|
NszsrkMELS0FC2viTw+NEU0ZnAT/aD5eydLCwMUEGBYKATcFgmlBP2kFiQWkj70J
|
|
EL3/QyKjLM3/RxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9y
|
|
Z1R29gVOHk9hllft18uHrwEV0MgvzykG3yWKF1B+vT9hApsCvqAEGRYKAG8FgmlB
|
|
P2kJEHx7uc2lNPhjRxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdw
|
|
Lm9yZ9Q8ycbljQVzg2YapbQX+IJ9H+1KSgSLXQtVtCsaJh8RFiEEerJu6BkdLdse
|
|
21ngfHu5zaU0+GMAAD3BAQCzmPr7q3Wf1BFKO95WBhe66Cg+lAnoJZKbgHBpUsIB
|
|
6wEAmGzOcL1zYXRBWpquzFKS2bNxuFdpbewkxkf0axvMnA8WIQSQO2vdjfwfK4J6
|
|
FaK9/0MioyzN/wAAoD8A/2a7+DrKEjL7fAt7UZnPYZOXPEOpYXxUE3ZGQbnsKrdw
|
|
AP0ae/B4LYXd89UwHpQUBEJHbY113W8NRUfpM/EJUQRdBM4zBGlBP2kWCSsGAQQB
|
|
2kcPAQEHQNhqbQtL5hS4w0n9H0hHMiD84WY39ukclu1yCtBii/+xwsDFBBgWCgE3
|
|
BYJpQT9pBYkFpI+9CRC9/0MioyzN/0cUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5z
|
|
ZXF1b2lhLXBncC5vcmd6G9Mcoj01NqY/TIgPlhe4L5OMUS/R4cIc2wWl2EmQTAKb
|
|
IL6gBBkWCgBvBYJpQT9pCRAR5pEu+j17CUcUAAAAAAAeACBzYWx0QG5vdGF0aW9u
|
|
cy5zZXF1b2lhLXBncC5vcmeJ47SGs3Kf1A20PTM/BFkWz2zd2dfaiVx8zKNbXoEZ
|
|
9BYhBK5UVQP/gdjl2RIb3RHmkS76PXsJAACjYgD/SfQ8WoCnBDnywpVknkW8FtHd
|
|
akewRjkT17Q5gK1NEWUA/jOMj8chhN4R6jnnWEb0ClujkkVMBWP7h2N5HWGN4DoD
|
|
FiEEkDtr3Y38HyuCehWivf9DIqMszf8AAC7NAQCNZWDduo/vseZGLDNOK9y+krFZ
|
|
fDSGVBmvBHGiGjX6agD/SHcsROm0C/HXe+KxS/SAhHob2KdZ8FQ+dCtr3oY/egXO
|
|
OARpQT9pEgorBgEEAZdVAQUBAQdA1dxpaBPWnqr8lhzucvP2F746IIdfYk+UVImg
|
|
pe3GD1UDAQgHwsAGBBgWCgB4BYJpQT9pBYkFpI+9CRC9/0MioyzN/0cUAAAAAAAe
|
|
ACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmeDJU2fBZAzwn32lcgdEyjB
|
|
/LtqMEQ7Qv5HwfJFRlseCAKbDBYhBJA7a92N/B8rgnoVor3/QyKjLM3/AADjHQEA
|
|
yvN5w7wxf50yriNlrHCwRzQ4MljfJawXfAfOthJlh8gBANycfude13YmUeFvwuyQ
|
|
Vmac4mRS69ivzHMLDNXUsMwB
|
|
=WPEh
|
|
-----END PGP PUBLIC KEY BLOCK-----
|
|
";
|
|
let body = Bytes::from(&body[..]);
|
|
assert!(! is_encrypted(&body));
|
|
|
|
// broken message
|
|
let body = b"-----BEGIN PGP MESSAGE-----
|
|
|
|
wWwGFQRhclhA/Qg3CiL81NAswyOWkyk8jxIBB0AmlBkVEVbQst60C/1+mcmIxLbG
|
|
W/g9mJDd0ral30VL0FfuKRR2Je2TRfizqMb4tL4EIRFqrfSBesb9WcniEfBekG3B
|
|
jk6lh01Ej9QTGQ==
|
|
-----END PGP MESSAGE-----
|
|
";
|
|
let body = Bytes::from(&body[..]);
|
|
assert!(! is_encrypted(&body));
|
|
|
|
}
|
|
|
|
}
|