mirror of
https://github.com/narrrl/proton-sdk-rs.git
synced 2026-11-03 16:41:26 +01:00
feat(sdk/account): support resolving public keys for authorship verification
This commit is contained in:
1 parent
edab8ff8e9
commit
fdfab3df3f
2 files changed
+108
-2
No files matched your search
@@ -17,13 +17,16 @@ use std::sync::Arc;
|
||||
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::crypto::{self, PrivateKey};
|
||||
use crate::crypto::{self, PrivateKey, PublicKey};
|
||||
use crate::error::{ProtonError, Result};
|
||||
use crate::http::ApiHttpClient;
|
||||
use crate::ids::AddressId;
|
||||
use crate::session::ProtonApiSession;
|
||||
|
||||
use dtos::{AddressDto, AddressListResponse, KeySaltListResponse, UserResponse};
|
||||
use dtos::{
|
||||
AddressDto, AddressListResponse, AddressPublicKeyListResponse, KeySaltListResponse,
|
||||
UserResponse,
|
||||
};
|
||||
|
||||
/// Public view of an email address attached to the account.
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -58,6 +61,8 @@ struct Cache {
|
||||
user_keys: Option<Vec<PrivateKey>>,
|
||||
addresses: Option<Vec<Address>>,
|
||||
address_keys: HashMap<AddressId, Vec<PrivateKey>>,
|
||||
/// email → active (non-compromised) public keys, for authorship verification.
|
||||
public_keys: HashMap<String, Vec<PublicKey>>,
|
||||
}
|
||||
|
||||
impl AccountClient {
|
||||
@@ -132,6 +137,61 @@ impl AccountClient {
|
||||
})
|
||||
}
|
||||
|
||||
/// Active public keys for an email address, used to verify authorship
|
||||
/// signatures. Mirrors C# `AddressOperations.GetPublicKeysAsync`:
|
||||
/// `core/v4/keys/all?InternalOnly=1&Email=…`, keeping only keys whose
|
||||
/// `Flags` mark them not-compromised. Cached per email.
|
||||
///
|
||||
/// Because verification is non-fatal, a resolution failure (unknown
|
||||
/// address, external domain, transport error) is logged and yields an empty
|
||||
/// key set rather than propagating — the caller then sees
|
||||
/// [`crate::crypto::VerificationStatus::NoVerifier`].
|
||||
pub async fn public_keys(&self, email: &str) -> Vec<PublicKey> {
|
||||
{
|
||||
let cache = self.inner.cache.lock().await;
|
||||
if let Some(keys) = cache.public_keys.get(email) {
|
||||
return keys.clone();
|
||||
}
|
||||
}
|
||||
|
||||
let path = format!(
|
||||
"core/v4/keys/all?InternalOnly=1&Email={}",
|
||||
encode_query_component(email)
|
||||
);
|
||||
let keys = match self
|
||||
.inner
|
||||
.http
|
||||
.get::<AddressPublicKeyListResponse>(&path)
|
||||
.await
|
||||
{
|
||||
Ok(response) => response
|
||||
.address
|
||||
.keys
|
||||
.iter()
|
||||
.filter(|entry| entry.is_not_compromised())
|
||||
.filter_map(|entry| match PublicKey::from_armored(&entry.public_key) {
|
||||
Ok(key) => Some(key),
|
||||
Err(e) => {
|
||||
tracing::warn!(%email, error = %e, "failed to parse public key");
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
Err(e) => {
|
||||
tracing::warn!(%email, error = %e, "failed to resolve public keys; treating as unverified");
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
|
||||
self.inner
|
||||
.cache
|
||||
.lock()
|
||||
.await
|
||||
.public_keys
|
||||
.insert(email.to_string(), keys.clone());
|
||||
keys
|
||||
}
|
||||
|
||||
/// Decrypted user (account) keys.
|
||||
async fn user_keys(&self) -> Result<Vec<PrivateKey>> {
|
||||
{
|
||||
@@ -265,3 +325,18 @@ fn decode_base64(value: &str) -> Result<Vec<u8>> {
|
||||
.decode(value)
|
||||
.map_err(|e| ProtonError::invalid_operation(format!("invalid base64 salt: {e}")))
|
||||
}
|
||||
|
||||
/// Percent-encode a value for use in a URL query component. Email addresses
|
||||
/// contain `@` (and may contain `+`), which must be escaped.
|
||||
fn encode_query_component(value: &str) -> String {
|
||||
let mut out = String::with_capacity(value.len());
|
||||
for byte in value.bytes() {
|
||||
match byte {
|
||||
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => {
|
||||
out.push(byte as char)
|
||||
}
|
||||
_ => out.push_str(&format!("%{byte:02X}")),
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
@@ -102,3 +102,34 @@ pub(super) struct KeySalt {
|
||||
#[serde(rename = "KeySalt")]
|
||||
pub value: Option<String>,
|
||||
}
|
||||
|
||||
/// Response for `core/v4/keys/all` — the active public keys for an email
|
||||
/// address. Mirrors C# `AddressPublicKeyListResponse`.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(super) struct AddressPublicKeyListResponse {
|
||||
#[serde(rename = "Address")]
|
||||
pub address: PublicKeyListAddress,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(super) struct PublicKeyListAddress {
|
||||
#[serde(rename = "Keys")]
|
||||
pub keys: Vec<PublicKeyEntry>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(super) struct PublicKeyEntry {
|
||||
/// Status flags; bit `1` = not compromised, bit `2` = not obsolete.
|
||||
#[serde(rename = "Flags")]
|
||||
pub flags: i32,
|
||||
#[serde(rename = "PublicKey")]
|
||||
pub public_key: String,
|
||||
}
|
||||
|
||||
impl PublicKeyEntry {
|
||||
/// Bit `1` of `Flags` (C# `PublicKeyStatus.IsNotCompromised`): only
|
||||
/// non-compromised keys are used to verify authorship.
|
||||
pub fn is_not_compromised(&self) -> bool {
|
||||
self.flags & 1 != 0
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user