mirror of
https://github.com/chatmail/core.git
synced 2026-04-20 15:06:30 +03:00
Closes https://github.com/chatmail/core/issues/7980.
Unpublished transports are not advertised to contacts, and self-sent messages are not sent there, so that we don't cause extra messages to the corresponding inbox, but can still receive messages from contacts who don't know the new relay addresses yet.
- This adds `list_transports_ex()` and `set_transport_unpublished()` JsonRPC functions
- By default, transports are published, but when updating, all existing transports except for the primary one become unpublished in order not to break existing users that followed https://delta.chat/legacy-move
- It is not possible to unpublish the primary transport, and setting a transport as primary automatically sets it to published
An alternative would be to change the existing list_transports API rather than adding a new one list_transports_ex. But to be honest, I don't mind the _ex prefix that much, and I am wary about compatibility issues. But maybe it would be fine; see b08ba4bb8 for how this would look.
898 lines
32 KiB
Rust
898 lines
32 KiB
Rust
//! Cryptographic key module.
|
|
|
|
use std::collections::BTreeMap;
|
|
use std::fmt;
|
|
use std::io::Cursor;
|
|
|
|
use anyhow::{Context as _, Result, bail, ensure};
|
|
use base64::Engine as _;
|
|
use deltachat_contact_tools::EmailAddress;
|
|
use pgp::composed::{Deserializable, SignedKeyDetails};
|
|
pub use pgp::composed::{SignedPublicKey, SignedSecretKey};
|
|
use pgp::crypto::aead::AeadAlgorithm;
|
|
use pgp::crypto::hash::HashAlgorithm;
|
|
use pgp::crypto::sym::SymmetricKeyAlgorithm;
|
|
use pgp::packet::{
|
|
Features, KeyFlags, Notation, PacketTrait as _, SignatureConfig, SignatureType, Subpacket,
|
|
SubpacketData,
|
|
};
|
|
use pgp::ser::Serialize;
|
|
use pgp::types::{CompressionAlgorithm, KeyDetails, KeyVersion};
|
|
use rand_old::thread_rng;
|
|
use tokio::runtime::Handle;
|
|
|
|
use crate::context::Context;
|
|
use crate::events::EventType;
|
|
use crate::log::LogExt;
|
|
use crate::tools::{self, time_elapsed};
|
|
|
|
/// Convenience trait for working with keys.
|
|
///
|
|
/// This trait is implemented for rPGP's [SignedPublicKey] and
|
|
/// [SignedSecretKey] types and makes working with them a little
|
|
/// easier in the deltachat world.
|
|
pub trait DcKey: Serialize + Deserializable + Clone {
|
|
/// Create a key from some bytes.
|
|
fn from_slice(bytes: &[u8]) -> Result<Self> {
|
|
let res = <Self as Deserializable>::from_bytes(Cursor::new(bytes));
|
|
if let Ok(res) = res {
|
|
return Ok(res);
|
|
}
|
|
|
|
// Workaround for keys imported using
|
|
// Delta Chat core < 1.0.0.
|
|
// Old Delta Chat core had a bug
|
|
// that resulted in treating CRC24 checksum
|
|
// as part of the key when reading ASCII Armor.
|
|
// Some users that started using Delta Chat in 2019
|
|
// have such corrupted keys with garbage bytes at the end.
|
|
//
|
|
// Garbage is at least 3 bytes long
|
|
// and may be longer due to padding
|
|
// at the end of the real key data
|
|
// and importing the key multiple times.
|
|
//
|
|
// If removing 10 bytes is not enough,
|
|
// the key is likely actually corrupted.
|
|
for garbage_bytes in 3..std::cmp::min(bytes.len(), 10) {
|
|
let res = <Self as Deserializable>::from_bytes(Cursor::new(
|
|
bytes
|
|
.get(..bytes.len().saturating_sub(garbage_bytes))
|
|
.unwrap_or_default(),
|
|
));
|
|
if let Ok(res) = res {
|
|
return Ok(res);
|
|
}
|
|
}
|
|
|
|
// Removing garbage bytes did not help, return the error.
|
|
Ok(res?)
|
|
}
|
|
|
|
/// Create a key from a base64 string.
|
|
fn from_base64(data: &str) -> Result<Self> {
|
|
// strip newlines and other whitespace
|
|
let cleaned: String = data.split_whitespace().collect();
|
|
let bytes = base64::engine::general_purpose::STANDARD.decode(cleaned.as_bytes())?;
|
|
Self::from_slice(&bytes)
|
|
}
|
|
|
|
/// Create a key from an ASCII-armored string.
|
|
fn from_asc(data: &str) -> Result<Self> {
|
|
let bytes = data.as_bytes();
|
|
let res = Self::from_armor_single(Cursor::new(bytes));
|
|
let (key, _headers) = match res {
|
|
Err(pgp::errors::Error::NoMatchingPacket { .. }) => match Self::is_private() {
|
|
true => bail!("No private key packet found"),
|
|
false => bail!("No public key packet found"),
|
|
},
|
|
_ => res.context("rPGP error")?,
|
|
};
|
|
Ok(key)
|
|
}
|
|
|
|
/// Serialise the key as bytes.
|
|
fn to_bytes(&self) -> Vec<u8> {
|
|
// Not using Serialize::to_bytes() to make clear *why* it is
|
|
// safe to ignore this error.
|
|
// Because we write to a Vec<u8> the io::Write impls never
|
|
// fail and we can hide this error.
|
|
let mut buf = Vec::new();
|
|
self.to_writer(&mut buf).unwrap();
|
|
buf
|
|
}
|
|
|
|
/// Serialise the key to a base64 string.
|
|
fn to_base64(&self) -> String {
|
|
base64::engine::general_purpose::STANDARD.encode(DcKey::to_bytes(self))
|
|
}
|
|
|
|
/// Serialise the key to ASCII-armored representation.
|
|
///
|
|
/// Each header line must be terminated by `\r\n`. Only allows setting one
|
|
/// header as a simplification since that's the only way it's used so far.
|
|
// Since .to_armored_string() are actual methods on SignedPublicKey and
|
|
// SignedSecretKey we can not generically implement this.
|
|
fn to_asc(&self, header: Option<(&str, &str)>) -> String;
|
|
|
|
/// The fingerprint for the key.
|
|
fn dc_fingerprint(&self) -> Fingerprint;
|
|
|
|
/// Whether the key is private (or public).
|
|
fn is_private() -> bool;
|
|
}
|
|
|
|
/// Converts secret key to public key.
|
|
pub(crate) fn secret_key_to_public_key(
|
|
context: &Context,
|
|
mut signed_secret_key: SignedSecretKey,
|
|
timestamp: u32,
|
|
addr: &str,
|
|
relay_addrs: &str,
|
|
) -> Result<SignedPublicKey> {
|
|
info!(context, "Converting secret key to public key.");
|
|
let timestamp = pgp::types::Timestamp::from_secs(timestamp);
|
|
|
|
// Subpackets that we want to share between DKS and User ID signature.
|
|
let common_subpackets = || -> Result<Vec<Subpacket>> {
|
|
let keyflags = {
|
|
let mut keyflags = KeyFlags::default();
|
|
keyflags.set_certify(true);
|
|
keyflags.set_sign(true);
|
|
keyflags
|
|
};
|
|
let features = {
|
|
let mut features = Features::default();
|
|
features.set_seipd_v1(true);
|
|
features.set_seipd_v2(true);
|
|
features
|
|
};
|
|
|
|
Ok(vec![
|
|
Subpacket::regular(SubpacketData::SignatureCreationTime(timestamp))?,
|
|
Subpacket::regular(SubpacketData::IssuerFingerprint(
|
|
signed_secret_key.fingerprint(),
|
|
))?,
|
|
Subpacket::regular(SubpacketData::KeyFlags(keyflags))?,
|
|
Subpacket::regular(SubpacketData::Features(features))?,
|
|
Subpacket::regular(SubpacketData::PreferredSymmetricAlgorithms(smallvec![
|
|
SymmetricKeyAlgorithm::AES256,
|
|
SymmetricKeyAlgorithm::AES192,
|
|
SymmetricKeyAlgorithm::AES128
|
|
]))?,
|
|
Subpacket::regular(SubpacketData::PreferredHashAlgorithms(smallvec![
|
|
HashAlgorithm::Sha256,
|
|
HashAlgorithm::Sha384,
|
|
HashAlgorithm::Sha512,
|
|
HashAlgorithm::Sha224,
|
|
]))?,
|
|
Subpacket::regular(SubpacketData::PreferredCompressionAlgorithms(smallvec![
|
|
CompressionAlgorithm::ZLIB,
|
|
CompressionAlgorithm::ZIP,
|
|
]))?,
|
|
Subpacket::regular(SubpacketData::PreferredAeadAlgorithms(smallvec![(
|
|
SymmetricKeyAlgorithm::AES256,
|
|
AeadAlgorithm::Ocb
|
|
)]))?,
|
|
Subpacket::regular(SubpacketData::IsPrimary(true))?,
|
|
])
|
|
};
|
|
|
|
// RFC 4880 required that Transferrable Public Key (aka OpenPGP Certificate)
|
|
// contains at least one User ID:
|
|
// <https://www.rfc-editor.org/rfc/rfc4880#section-11.1>
|
|
// RFC 9580 does not require User ID even for V4 certificates anymore:
|
|
// <https://www.rfc-editor.org/rfc/rfc9580.html#name-openpgp-version-4-certifica>
|
|
//
|
|
// We do not use and do not expect User ID in any keys,
|
|
// but nevertheless include User ID in V4 keys for compatibility with clients that follow RFC 4880.
|
|
// RFC 9580 also recommends including User ID into V4 keys:
|
|
// <https://www.rfc-editor.org/rfc/rfc9580.html#section-5.2.3.10-8>
|
|
//
|
|
// We do not support keys older than V4 and are not going
|
|
// to include User ID in newer V6 keys as all clients that support V6
|
|
// should support keys without User ID.
|
|
let users = if signed_secret_key.version() == KeyVersion::V4 {
|
|
let user_id = format!("<{addr}>");
|
|
|
|
let mut rng = thread_rng();
|
|
// Self-signature is a "positive certification",
|
|
// see <https://www.ietf.org/archive/id/draft-gallagher-openpgp-signatures-02.html#name-certification-signature-typ>.
|
|
let mut user_id_signature_config = SignatureConfig::from_key(
|
|
&mut rng,
|
|
&signed_secret_key.primary_key,
|
|
SignatureType::CertPositive,
|
|
)?;
|
|
user_id_signature_config.hashed_subpackets = common_subpackets()?;
|
|
user_id_signature_config.unhashed_subpackets = vec![Subpacket::regular(
|
|
SubpacketData::IssuerKeyId(signed_secret_key.legacy_key_id()),
|
|
)?];
|
|
let user_id_packet =
|
|
pgp::packet::UserId::from_str(pgp::types::PacketHeaderVersion::New, &user_id)?;
|
|
let signature = user_id_signature_config.sign_certification(
|
|
&signed_secret_key.primary_key,
|
|
&signed_secret_key.primary_key.public_key(),
|
|
&pgp::types::Password::empty(),
|
|
user_id_packet.tag(),
|
|
&user_id_packet,
|
|
)?;
|
|
vec![user_id_packet.into_signed(signature)]
|
|
} else {
|
|
vec![]
|
|
};
|
|
|
|
let direct_signatures = {
|
|
let mut rng = thread_rng();
|
|
let mut direct_key_signature_config = SignatureConfig::from_key(
|
|
&mut rng,
|
|
&signed_secret_key.primary_key,
|
|
SignatureType::Key,
|
|
)?;
|
|
direct_key_signature_config.hashed_subpackets = common_subpackets()?;
|
|
let notation = Notation {
|
|
readable: true,
|
|
name: "relays@chatmail.at".into(),
|
|
value: relay_addrs.to_string().into(),
|
|
};
|
|
direct_key_signature_config
|
|
.hashed_subpackets
|
|
.push(Subpacket::regular(SubpacketData::Notation(notation))?);
|
|
let direct_key_signature = direct_key_signature_config.sign_key(
|
|
&signed_secret_key.primary_key,
|
|
&pgp::types::Password::empty(),
|
|
signed_secret_key.primary_key.public_key(),
|
|
)?;
|
|
vec![direct_key_signature]
|
|
};
|
|
|
|
signed_secret_key.details = SignedKeyDetails {
|
|
revocation_signatures: vec![],
|
|
direct_signatures,
|
|
users,
|
|
user_attributes: vec![],
|
|
};
|
|
|
|
Ok(signed_secret_key.to_public_key())
|
|
}
|
|
|
|
/// Attempts to load own public key.
|
|
///
|
|
/// Returns `None` if no secret key is generated yet.
|
|
pub(crate) async fn load_self_public_key_opt(context: &Context) -> Result<Option<SignedPublicKey>> {
|
|
let mut lock = context.self_public_key.lock().await;
|
|
|
|
if let Some(ref public_key) = *lock {
|
|
return Ok(Some(public_key.clone()));
|
|
}
|
|
|
|
let Some(secret_key_bytes) = context
|
|
.sql
|
|
.query_row_optional(
|
|
"SELECT private_key
|
|
FROM keypairs
|
|
WHERE id=(SELECT value FROM config WHERE keyname='key_id')",
|
|
(),
|
|
|row| {
|
|
let bytes: Vec<u8> = row.get(0)?;
|
|
Ok(bytes)
|
|
},
|
|
)
|
|
.await?
|
|
else {
|
|
return Ok(None);
|
|
};
|
|
let signed_secret_key = SignedSecretKey::from_slice(&secret_key_bytes)?;
|
|
let timestamp = context
|
|
.sql
|
|
.query_get_value::<u32>(
|
|
"SELECT MAX(timestamp)
|
|
FROM (SELECT add_timestamp AS timestamp
|
|
FROM transports
|
|
UNION ALL
|
|
SELECT remove_timestamp AS timestamp
|
|
FROM removed_transports)",
|
|
(),
|
|
)
|
|
.await?
|
|
.context("No transports configured")?;
|
|
let addr = context.get_primary_self_addr().await?;
|
|
let all_addrs = context.get_published_self_addrs().await?.join(",");
|
|
let signed_public_key =
|
|
secret_key_to_public_key(context, signed_secret_key, timestamp, &addr, &all_addrs)?;
|
|
*lock = Some(signed_public_key.clone());
|
|
|
|
Ok(Some(signed_public_key))
|
|
}
|
|
|
|
/// Loads own public key.
|
|
///
|
|
/// If no key is generated yet, generates a new one.
|
|
pub(crate) async fn load_self_public_key(context: &Context) -> Result<SignedPublicKey> {
|
|
match load_self_public_key_opt(context).await? {
|
|
Some(public_key) => Ok(public_key),
|
|
None => {
|
|
generate_keypair(context).await?;
|
|
let public_key = load_self_public_key_opt(context)
|
|
.await?
|
|
.context("Secret key generated, but public key cannot be created")?;
|
|
Ok(public_key)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns our own public keyring.
|
|
///
|
|
/// No keys are generated and at most one key is returned.
|
|
pub(crate) async fn load_self_public_keyring(context: &Context) -> Result<Vec<SignedPublicKey>> {
|
|
if let Some(public_key) = load_self_public_key_opt(context).await? {
|
|
Ok(vec![public_key])
|
|
} else {
|
|
Ok(vec![])
|
|
}
|
|
}
|
|
|
|
/// Returns own public key fingerprint in (not human-readable) hex representation.
|
|
/// This is the fingerprint format that is used in the database.
|
|
///
|
|
/// If no key is generated yet, generates a new one.
|
|
///
|
|
/// For performance reasons, the fingerprint is cached after the first invocation.
|
|
pub(crate) async fn self_fingerprint(context: &Context) -> Result<&str> {
|
|
if let Some(fp) = context.self_fingerprint.get() {
|
|
Ok(fp)
|
|
} else {
|
|
let fp = load_self_public_key(context).await?.dc_fingerprint().hex();
|
|
Ok(context.self_fingerprint.get_or_init(|| fp))
|
|
}
|
|
}
|
|
|
|
/// Returns own public key fingerprint in (not human-readable) hex representation.
|
|
/// This is the fingerprint format that is used in the database.
|
|
///
|
|
/// Returns `None` if no key is generated yet.
|
|
///
|
|
/// For performance reasons, the fingerprint is cached after the first invocation.
|
|
pub(crate) async fn self_fingerprint_opt(context: &Context) -> Result<Option<&str>> {
|
|
if let Some(fp) = context.self_fingerprint.get() {
|
|
Ok(Some(fp))
|
|
} else if let Some(key) = load_self_public_key_opt(context).await? {
|
|
let fp = key.dc_fingerprint().hex();
|
|
Ok(Some(context.self_fingerprint.get_or_init(|| fp)))
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
}
|
|
|
|
pub(crate) async fn load_self_secret_key(context: &Context) -> Result<SignedSecretKey> {
|
|
let private_key = context
|
|
.sql
|
|
.query_row_optional(
|
|
"SELECT private_key
|
|
FROM keypairs
|
|
WHERE id=(SELECT value FROM config WHERE keyname='key_id')",
|
|
(),
|
|
|row| {
|
|
let bytes: Vec<u8> = row.get(0)?;
|
|
Ok(bytes)
|
|
},
|
|
)
|
|
.await?;
|
|
match private_key {
|
|
Some(bytes) => SignedSecretKey::from_slice(&bytes),
|
|
None => {
|
|
let secret = generate_keypair(context).await?;
|
|
Ok(secret)
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) async fn load_self_secret_keyring(context: &Context) -> Result<Vec<SignedSecretKey>> {
|
|
let keys = context
|
|
.sql
|
|
.query_map_vec(
|
|
r#"SELECT private_key
|
|
FROM keypairs
|
|
ORDER BY id=(SELECT value FROM config WHERE keyname='key_id') DESC"#,
|
|
(),
|
|
|row| {
|
|
let bytes: Vec<u8> = row.get(0)?;
|
|
Ok(bytes)
|
|
},
|
|
)
|
|
.await?
|
|
.into_iter()
|
|
.filter_map(|bytes| SignedSecretKey::from_slice(&bytes).log_err(context).ok())
|
|
.collect();
|
|
Ok(keys)
|
|
}
|
|
|
|
impl DcKey for SignedPublicKey {
|
|
fn to_asc(&self, header: Option<(&str, &str)>) -> String {
|
|
// Not using .to_armored_string() to make clear *why* it is
|
|
// safe to ignore this error.
|
|
// Because we write to a Vec<u8> the io::Write impls never
|
|
// fail and we can hide this error.
|
|
let headers =
|
|
header.map(|(key, value)| BTreeMap::from([(key.to_string(), vec![value.to_string()])]));
|
|
let mut buf = Vec::new();
|
|
self.to_armored_writer(&mut buf, headers.as_ref().into())
|
|
.unwrap_or_default();
|
|
std::string::String::from_utf8(buf).unwrap_or_default()
|
|
}
|
|
|
|
fn is_private() -> bool {
|
|
false
|
|
}
|
|
|
|
fn dc_fingerprint(&self) -> Fingerprint {
|
|
self.fingerprint().into()
|
|
}
|
|
}
|
|
|
|
impl DcKey for SignedSecretKey {
|
|
fn to_asc(&self, header: Option<(&str, &str)>) -> String {
|
|
// Not using .to_armored_string() to make clear *why* it is
|
|
// safe to do these unwraps.
|
|
// Because we write to a Vec<u8> the io::Write impls never
|
|
// fail and we can hide this error. The string is always ASCII.
|
|
let headers =
|
|
header.map(|(key, value)| BTreeMap::from([(key.to_string(), vec![value.to_string()])]));
|
|
let mut buf = Vec::new();
|
|
self.to_armored_writer(&mut buf, headers.as_ref().into())
|
|
.unwrap_or_default();
|
|
std::string::String::from_utf8(buf).unwrap_or_default()
|
|
}
|
|
|
|
fn is_private() -> bool {
|
|
true
|
|
}
|
|
|
|
fn dc_fingerprint(&self) -> Fingerprint {
|
|
self.fingerprint().into()
|
|
}
|
|
}
|
|
|
|
async fn generate_keypair(context: &Context) -> Result<SignedSecretKey> {
|
|
let addr = context.get_primary_self_addr().await?;
|
|
let addr = EmailAddress::new(&addr)?;
|
|
let _public_key_guard = context.self_public_key.lock().await;
|
|
|
|
// Check if the key appeared while we were waiting on the lock.
|
|
match load_keypair(context).await? {
|
|
Some(key_pair) => Ok(key_pair),
|
|
None => {
|
|
let start = tools::Time::now();
|
|
info!(context, "Generating keypair.");
|
|
let keypair = Handle::current()
|
|
.spawn_blocking(move || crate::pgp::create_keypair(addr))
|
|
.await??;
|
|
|
|
store_self_keypair(context, &keypair).await?;
|
|
info!(
|
|
context,
|
|
"Keypair generated in {:.3}s.",
|
|
time_elapsed(&start).as_secs(),
|
|
);
|
|
Ok(keypair)
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) async fn load_keypair(context: &Context) -> Result<Option<SignedSecretKey>> {
|
|
let res = context
|
|
.sql
|
|
.query_row_optional(
|
|
"SELECT private_key
|
|
FROM keypairs
|
|
WHERE id=(SELECT value FROM config WHERE keyname='key_id')",
|
|
(),
|
|
|row| {
|
|
let sec_bytes: Vec<u8> = row.get(0)?;
|
|
Ok(sec_bytes)
|
|
},
|
|
)
|
|
.await?;
|
|
|
|
let signed_secret_key = if let Some(sec_bytes) = res {
|
|
Some(SignedSecretKey::from_slice(&sec_bytes)?)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
Ok(signed_secret_key)
|
|
}
|
|
|
|
/// Stores own keypair in the database and sets it as a default.
|
|
///
|
|
/// Fails if we already have a key, so it is not possible to
|
|
/// have more than one key for new setups. Existing setups
|
|
/// may still have more than one key for compatibility.
|
|
pub(crate) async fn store_self_keypair(
|
|
context: &Context,
|
|
signed_secret_key: &SignedSecretKey,
|
|
) -> Result<()> {
|
|
// This public key is stored in the database
|
|
// only for backwards compatibility.
|
|
//
|
|
// It should not be used e.g. in Autocrypt headers or vCards.
|
|
// Use `secret_key_to_public_key()` function instead,
|
|
// which adds relay list to the signature.
|
|
let signed_public_key = signed_secret_key.to_public_key();
|
|
let mut config_cache_lock = context.sql.config_cache.write().await;
|
|
let new_key_id = context
|
|
.sql
|
|
.transaction(|transaction| {
|
|
let public_key = DcKey::to_bytes(&signed_public_key);
|
|
let secret_key = DcKey::to_bytes(signed_secret_key);
|
|
|
|
// private_key and public_key columns
|
|
// are UNIQUE since migration 107,
|
|
// so this fails if we already have this key.
|
|
transaction
|
|
.execute(
|
|
"INSERT INTO keypairs (public_key, private_key)
|
|
VALUES (?,?)",
|
|
(&public_key, &secret_key),
|
|
)
|
|
.context("Failed to insert keypair")?;
|
|
|
|
let new_key_id = transaction.last_insert_rowid();
|
|
|
|
// This will fail if we already have `key_id`.
|
|
//
|
|
// Setting default key is only possible if we don't
|
|
// have a key already.
|
|
transaction.execute(
|
|
"INSERT INTO config (keyname, value) VALUES ('key_id', ?)",
|
|
(new_key_id,),
|
|
)?;
|
|
Ok(new_key_id)
|
|
})
|
|
.await?;
|
|
context.emit_event(EventType::AccountsItemChanged);
|
|
config_cache_lock.insert("key_id".to_string(), Some(new_key_id.to_string()));
|
|
Ok(())
|
|
}
|
|
|
|
/// Saves a keypair as the default keys.
|
|
///
|
|
/// This API is used for testing purposes
|
|
/// to avoid generating the key in tests.
|
|
/// Use import/export APIs instead.
|
|
pub async fn preconfigure_keypair(context: &Context, secret_data: &str) -> Result<()> {
|
|
let secret = SignedSecretKey::from_asc(secret_data)?;
|
|
store_self_keypair(context, &secret).await?;
|
|
Ok(())
|
|
}
|
|
|
|
/// A key fingerprint
|
|
#[derive(Clone, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)]
|
|
pub struct Fingerprint(Vec<u8>);
|
|
|
|
impl Fingerprint {
|
|
/// Creates new 160-bit (20 bytes) fingerprint.
|
|
pub fn new(v: Vec<u8>) -> Fingerprint {
|
|
debug_assert_eq!(v.len(), 20);
|
|
Fingerprint(v)
|
|
}
|
|
|
|
/// Make a hex string from the fingerprint.
|
|
///
|
|
/// Use [std::fmt::Display] or [ToString::to_string] to get a
|
|
/// human-readable formatted string.
|
|
pub fn hex(&self) -> String {
|
|
hex::encode_upper(&self.0)
|
|
}
|
|
}
|
|
|
|
impl From<pgp::types::Fingerprint> for Fingerprint {
|
|
fn from(fingerprint: pgp::types::Fingerprint) -> Fingerprint {
|
|
Self::new(fingerprint.as_bytes().into())
|
|
}
|
|
}
|
|
|
|
impl fmt::Debug for Fingerprint {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
f.debug_struct("Fingerprint")
|
|
.field("hex", &self.hex())
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
/// Make a human-readable fingerprint.
|
|
impl fmt::Display for Fingerprint {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
// Split key into chunks of 4 with space and newline at 20 chars
|
|
for (i, c) in self.hex().chars().enumerate() {
|
|
if i > 0 && i % 20 == 0 {
|
|
writeln!(f)?;
|
|
} else if i > 0 && i % 4 == 0 {
|
|
write!(f, " ")?;
|
|
}
|
|
write!(f, "{c}")?;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Parse a human-readable or otherwise formatted fingerprint.
|
|
impl std::str::FromStr for Fingerprint {
|
|
type Err = anyhow::Error;
|
|
|
|
fn from_str(input: &str) -> Result<Self> {
|
|
let hex_repr: String = input
|
|
.to_uppercase()
|
|
.chars()
|
|
.filter(|&c| c.is_ascii_hexdigit())
|
|
.collect();
|
|
let v: Vec<u8> = hex::decode(&hex_repr)?;
|
|
ensure!(v.len() == 20, "wrong fingerprint length: {hex_repr}");
|
|
let fp = Fingerprint::new(v);
|
|
Ok(fp)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::sync::{Arc, LazyLock};
|
|
|
|
use super::*;
|
|
use crate::config::Config;
|
|
use crate::test_utils::{TestContext, alice_keypair};
|
|
|
|
static KEYPAIR: LazyLock<SignedSecretKey> = LazyLock::new(alice_keypair);
|
|
|
|
#[test]
|
|
fn test_from_armored_string() {
|
|
let private_key = SignedSecretKey::from_asc(
|
|
"-----BEGIN PGP PRIVATE KEY BLOCK-----
|
|
|
|
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
|
|
7yPJeQ==
|
|
=KZk/
|
|
-----END PGP PRIVATE KEY BLOCK-----",
|
|
)
|
|
.expect("failed to decode");
|
|
let binary = DcKey::to_bytes(&private_key);
|
|
SignedSecretKey::from_slice(&binary).expect("invalid private key");
|
|
}
|
|
|
|
#[test]
|
|
fn test_asc_roundtrip() {
|
|
let key = KEYPAIR.clone().to_public_key();
|
|
let asc = key.to_asc(Some(("spam", "ham")));
|
|
let key2 = SignedPublicKey::from_asc(&asc).unwrap();
|
|
assert_eq!(key, key2);
|
|
|
|
let key = KEYPAIR.clone();
|
|
let asc = key.to_asc(Some(("spam", "ham")));
|
|
let key2 = SignedSecretKey::from_asc(&asc).unwrap();
|
|
assert_eq!(key, key2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_from_slice_roundtrip() {
|
|
let private_key = KEYPAIR.clone();
|
|
let public_key = KEYPAIR.clone().to_public_key();
|
|
|
|
let binary = DcKey::to_bytes(&public_key);
|
|
let public_key2 = SignedPublicKey::from_slice(&binary).expect("invalid public key");
|
|
assert_eq!(public_key, public_key2);
|
|
|
|
let binary = DcKey::to_bytes(&private_key);
|
|
let private_key2 = SignedSecretKey::from_slice(&binary).expect("invalid private key");
|
|
assert_eq!(private_key, private_key2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_from_slice_bad_data() {
|
|
let mut bad_data: [u8; 4096] = [0; 4096];
|
|
for (i, v) in bad_data.iter_mut().enumerate() {
|
|
*v = (i & 0xff) as u8;
|
|
}
|
|
for j in 0..(4096 / 40) {
|
|
let slice = &bad_data.get(j..j + 4096 / 2 + j).unwrap();
|
|
assert!(SignedPublicKey::from_slice(slice).is_err());
|
|
assert!(SignedSecretKey::from_slice(slice).is_err());
|
|
}
|
|
}
|
|
|
|
/// Tests workaround for Delta Chat core < 1.0.0
|
|
/// which parsed CRC24 at the end of ASCII Armor
|
|
/// as the part of the key.
|
|
/// Depending on the alignment and the number of
|
|
/// `=` characters at the end of the key,
|
|
/// this resulted in various number of garbage
|
|
/// octets at the end of the key, starting from 3 octets,
|
|
/// but possibly 4 or 5 and maybe more octets
|
|
/// if the key is imported multiple times.
|
|
#[test]
|
|
fn test_ignore_trailing_garbage() {
|
|
// Test several variants of garbage.
|
|
for garbage in [
|
|
b"\x02\xfc\xaa\x38\x4b\x5c".as_slice(),
|
|
b"\x02\xfc\xaa".as_slice(),
|
|
b"\x01\x02\x03\x04\x05".as_slice(),
|
|
] {
|
|
let private_key = KEYPAIR.clone();
|
|
|
|
let mut binary = DcKey::to_bytes(&private_key);
|
|
binary.extend(garbage);
|
|
|
|
let private_key2 =
|
|
SignedSecretKey::from_slice(&binary).expect("Failed to ignore garbage");
|
|
|
|
assert_eq!(private_key.dc_fingerprint(), private_key2.dc_fingerprint());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_base64_roundtrip() {
|
|
let key = KEYPAIR.clone().to_public_key();
|
|
let base64 = key.to_base64();
|
|
let key2 = SignedPublicKey::from_base64(&base64).unwrap();
|
|
assert_eq!(key, key2);
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_load_self_generate_public() {
|
|
let t = TestContext::new().await;
|
|
t.set_config(Config::ConfiguredAddr, Some("alice@example.org"))
|
|
.await
|
|
.unwrap();
|
|
let key = load_self_public_key(&t).await;
|
|
assert!(key.is_ok());
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_load_self_generate_secret() {
|
|
let t = TestContext::new().await;
|
|
t.set_config(Config::ConfiguredAddr, Some("alice@example.org"))
|
|
.await
|
|
.unwrap();
|
|
let key = load_self_secret_key(&t).await;
|
|
assert!(key.is_ok());
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_load_self_generate_concurrent() {
|
|
use std::thread;
|
|
|
|
let t = TestContext::new().await;
|
|
t.set_config(Config::ConfiguredAddr, Some("alice@example.org"))
|
|
.await
|
|
.unwrap();
|
|
let thr0 = {
|
|
let ctx = t.clone();
|
|
thread::spawn(move || {
|
|
tokio::runtime::Runtime::new()
|
|
.unwrap()
|
|
.block_on(load_self_public_key(&ctx))
|
|
})
|
|
};
|
|
let thr1 = {
|
|
let ctx = t.clone();
|
|
thread::spawn(move || {
|
|
tokio::runtime::Runtime::new()
|
|
.unwrap()
|
|
.block_on(load_self_public_key(&ctx))
|
|
})
|
|
};
|
|
let res0 = thr0.join().unwrap();
|
|
let res1 = thr1.join().unwrap();
|
|
assert_eq!(res0.unwrap(), res1.unwrap());
|
|
}
|
|
|
|
/// Tests that setting a default key second time is not allowed.
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_save_self_key_twice() {
|
|
// Saving the same key twice should result in only one row in
|
|
// the keypairs table.
|
|
let t = TestContext::new().await;
|
|
let ctx = Arc::new(t);
|
|
|
|
let nrows = || async {
|
|
ctx.sql
|
|
.count("SELECT COUNT(*) FROM keypairs;", ())
|
|
.await
|
|
.unwrap()
|
|
};
|
|
assert_eq!(nrows().await, 0);
|
|
store_self_keypair(&ctx, &KEYPAIR).await.unwrap();
|
|
assert_eq!(nrows().await, 1);
|
|
|
|
// Saving a second key fails.
|
|
let res = store_self_keypair(&ctx, &KEYPAIR).await;
|
|
assert!(res.is_err());
|
|
|
|
assert_eq!(nrows().await, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_fingerprint_from_str() {
|
|
let res = Fingerprint::new(vec![
|
|
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
|
|
]);
|
|
|
|
let fp: Fingerprint = "0102030405060708090A0B0c0d0e0F1011121314".parse().unwrap();
|
|
assert_eq!(fp, res);
|
|
|
|
let fp: Fingerprint = "zzzz 0102 0304 0506\n0708090a0b0c0D0E0F1011121314 yyy"
|
|
.parse()
|
|
.unwrap();
|
|
assert_eq!(fp, res);
|
|
|
|
assert!("1".parse::<Fingerprint>().is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_fingerprint_hex() {
|
|
let fp = Fingerprint::new(vec![
|
|
1, 2, 4, 8, 16, 32, 64, 128, 255, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
|
|
]);
|
|
assert_eq!(fp.hex(), "0102040810204080FF0A0B0C0D0E0F1011121314");
|
|
}
|
|
|
|
#[test]
|
|
fn test_fingerprint_to_string() {
|
|
let fp = Fingerprint::new(vec![
|
|
1, 2, 4, 8, 16, 32, 64, 128, 255, 1, 2, 4, 8, 16, 32, 64, 128, 255, 19, 20,
|
|
]);
|
|
assert_eq!(
|
|
fp.to_string(),
|
|
"0102 0408 1020 4080 FF01\n0204 0810 2040 80FF 1314"
|
|
);
|
|
}
|
|
}
|