From e2f5ab7ad680d4c7dc51c608b01dcf16ff194da6 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Fri, 26 Jun 2026 20:47:06 +0200 Subject: [PATCH] fmt + Clippy --- .../proton-drive-sdk/examples/live_login.rs | 8 +- crates/proton-drive-sdk/src/cache.rs | 19 ++- crates/proton-drive-sdk/src/client.rs | 158 +++++++++++------- crates/proton-drive-sdk/src/crypto.rs | 10 +- crates/proton-drive-sdk/src/dtos.rs | 10 +- crates/proton-drive-sdk/src/events.rs | 10 +- crates/proton-drive-sdk/src/lib.rs | 6 +- crates/proton-drive-sdk/src/node.rs | 5 +- crates/proton-sdk/src/account.rs | 28 ++-- crates/proton-sdk/src/cache.rs | 10 +- crates/proton-sdk/src/crypto/content.rs | 56 +++++-- crates/proton-sdk/src/crypto/encrypt.rs | 14 +- crates/proton-sdk/src/crypto/keys.rs | 14 +- crates/proton-sdk/src/crypto/messages.rs | 8 +- crates/proton-sdk/src/crypto/srp.rs | 21 +-- crates/proton-sdk/src/crypto/verify.rs | 16 +- crates/proton-sdk/src/http.rs | 28 +++- 17 files changed, 263 insertions(+), 158 deletions(-) diff --git a/crates/proton-drive-sdk/examples/live_login.rs b/crates/proton-drive-sdk/examples/live_login.rs index ab4acd6..1bd51e2 100644 --- a/crates/proton-drive-sdk/examples/live_login.rs +++ b/crates/proton-drive-sdk/examples/live_login.rs @@ -54,13 +54,13 @@ async fn main() -> Result<(), Box> { let root = client.get_my_files_folder().await?; eprintln!( "[+] root: name={:?} kind={:?} uid={}", - root.name, - root.kind, - root.uid, + root.name, root.kind, root.uid, ); eprintln!("[*] enumerating children ..."); - let child_uids = client.enumerate_folder_children_node_uids(&root.uid).await?; + let child_uids = client + .enumerate_folder_children_node_uids(&root.uid) + .await?; let children = client.enumerate_nodes(&child_uids).await?; eprintln!("[+] {} children:", children.len()); for c in &children { diff --git a/crates/proton-drive-sdk/src/cache.rs b/crates/proton-drive-sdk/src/cache.rs index c1f537a..d09c4db 100644 --- a/crates/proton-drive-sdk/src/cache.rs +++ b/crates/proton-drive-sdk/src/cache.rs @@ -194,14 +194,23 @@ mod tests { assert!(cache.main_volume_id().await.unwrap().is_none()); cache.set_client_uid("client-xyz").await.unwrap(); - cache.set_main_volume_id(&VolumeId::from("vol-1")).await.unwrap(); + cache + .set_main_volume_id(&VolumeId::from("vol-1")) + .await + .unwrap(); cache .set_my_files_share_id(&ShareId::from("mf-share")) .await .unwrap(); - assert_eq!(cache.client_uid().await.unwrap().as_deref(), Some("client-xyz")); - assert_eq!(cache.main_volume_id().await.unwrap(), Some(VolumeId::from("vol-1"))); + assert_eq!( + cache.client_uid().await.unwrap().as_deref(), + Some("client-xyz") + ); + assert_eq!( + cache.main_volume_id().await.unwrap(), + Some(VolumeId::from("vol-1")) + ); assert_eq!( cache.my_files_share_id().await.unwrap(), Some(ShareId::from("mf-share")) @@ -211,7 +220,9 @@ mod tests { #[tokio::test] async fn malformed_entry_is_evicted_as_miss() { let repo = InMemoryCacheRepository::shared(); - repo.set(&node_key(&uid("v", "l")), "not json", &[]).await.unwrap(); + repo.set(&node_key(&uid("v", "l")), "not json", &[]) + .await + .unwrap(); let cache = DriveEntityCache::new(repo.clone()); assert!(cache.try_get_node(&uid("v", "l")).await.unwrap().is_none()); // The bad entry was removed. diff --git a/crates/proton-drive-sdk/src/client.rs b/crates/proton-drive-sdk/src/client.rs index 796b5f6..70d2d1c 100644 --- a/crates/proton-drive-sdk/src/client.rs +++ b/crates/proton-drive-sdk/src/client.rs @@ -12,12 +12,12 @@ use tokio::sync::Mutex; use proton_sdk::account::AccountClient; use proton_sdk::cache::{CacheRepository, InMemoryCacheRepository}; use proton_sdk::crypto::PrivateKey; -use proton_sdk::error::{ProtonError, Result}; -use proton_sdk::http::ApiHttpClient; use proton_sdk::crypto::{ build_volume_creation_material, generate_node_hash_key, generate_node_key, verify_detached, ContentKey, VerificationKeyRing, VerificationStatus, }; +use proton_sdk::error::{ProtonError, Result}; +use proton_sdk::http::ApiHttpClient; use proton_sdk::ids::{AddressId, DriveEventId, LinkId, NodeUid, ShareId, VolumeId}; use proton_sdk::session::ProtonApiSession; use proton_sdk::telemetry::{NoopTelemetry, Telemetry, TelemetryExt}; @@ -25,6 +25,7 @@ use proton_sdk::telemetry::{NoopTelemetry, Telemetry, TelemetryExt}; use hmac::{Hmac, Mac}; use sha1::Sha1; +use crate::cache::DriveEntityCache; use crate::crypto::{ decrypt_content_key_verified, decrypt_extended_attributes_verified, decrypt_link, decrypt_link_verified, decrypt_share_key, @@ -34,19 +35,18 @@ use crate::dtos::{ BlockUploadPreparationResponse, BlockVerificationInputResponse, BlockVerifier, CommonExtendedAttributes, ExtendedAttributes, FileContentDigests, FileCreationRequest, FileCreationResponse, FolderChildrenResponse, FolderCreationRequest, FolderCreationResponse, - LinkDetailsDto, LinkDetailsRequest, LinkDetailsResponse, LinkDto, LinkType, MoveLinkRequest, - MoveMultipleLinksItem, MoveMultipleLinksRequest, MultipleLinksRequest, MyFilesShareResponse, - NodeNameAvailabilityRequest, - NodeNameAvailabilityResponse, RenameLinkRequest, RevisionCreationRequest, + LatestVolumeEventResponse, LinkDetailsDto, LinkDetailsRequest, LinkDetailsResponse, LinkDto, + LinkType, MoveLinkRequest, MoveMultipleLinksItem, MoveMultipleLinksRequest, + MultipleLinksRequest, MyFilesShareResponse, NodeNameAvailabilityRequest, + NodeNameAvailabilityResponse, PhotosAttributesDto, RenameLinkRequest, RevisionCreationRequest, RevisionCreationResponse, RevisionDto, RevisionResponse, RevisionUpdateRequest, - LatestVolumeEventResponse, PhotosAttributesDto, ThumbnailBlockListRequest, - ThumbnailBlockListResponse, ThumbnailCreationRequest, ThumbnailDto, TimelinePhotoListResponse, - VolumeCreationRequest, VolumeEventDto, VolumeEventListResponse, VolumeTrashResponse, + ThumbnailBlockListRequest, ThumbnailBlockListResponse, ThumbnailCreationRequest, ThumbnailDto, + TimelinePhotoListResponse, VolumeCreationRequest, VolumeEventDto, VolumeEventListResponse, + VolumeTrashResponse, }; -use crate::photos::{PhotoUploadMetadata, PhotosTimelineItem}; -use crate::cache::DriveEntityCache; use crate::events::{DriveEvent, DriveEventScopeId}; use crate::node::{FileThumbnail, Node, NodeKind, Thumbnail, ThumbnailType}; +use crate::photos::{PhotoUploadMetadata, PhotosTimelineItem}; /// Content blocks are 4 MiB of plaintext each (C# `RevisionWriter.DefaultBlockSize`). const DEFAULT_BLOCK_SIZE: usize = 1 << 22; @@ -223,7 +223,9 @@ impl ProtonDriveClient { let parent_key = self .resolve_parent_key(&uid.volume_id, &details.link) .await?; - let node = self.build_node(&uid.volume_id, &details, &parent_key).await?; + let node = self + .build_node(&uid.volume_id, &details, &parent_key) + .await?; timer.success(); Ok(Some(node)) } @@ -326,8 +328,7 @@ impl ProtonDriveClient { let mut page = 0_usize; loop { - let path = - format!("volumes/{volume_id}/trash?pageSize={TRASH_PAGE_SIZE}&page={page}"); + let path = format!("volumes/{volume_id}/trash?pageSize={TRASH_PAGE_SIZE}&page={page}"); let response: VolumeTrashResponse = self.http.get(&path).await?; let mut count = 0_usize; @@ -474,9 +475,9 @@ impl ProtonDriveClient { let content_key_packet_b64 = file.content_key_packet.ok_or_else(|| { ProtonError::invalid_operation("file is missing its content key packet") })?; - let content_key_packet = BASE64 - .decode(content_key_packet_b64.trim()) - .map_err(|e| ProtonError::invalid_operation(format!("decode content key packet: {e}")))?; + let content_key_packet = BASE64.decode(content_key_packet_b64.trim()).map_err(|e| { + ProtonError::invalid_operation(format!("decode content key packet: {e}")) + })?; let revision_id = file .active_revision .map(|r| r.id) @@ -536,7 +537,8 @@ impl ProtonDriveClient { uid: &NodeUid, thumbnail_type: ThumbnailType, ) -> Result>> { - self.download_thumbnail_ctx(uid, thumbnail_type, false).await + self.download_thumbnail_ctx(uid, thumbnail_type, false) + .await } /// As [`download_thumbnail`](Self::download_thumbnail), but routes node and @@ -646,10 +648,7 @@ impl ProtonDriveClient { } Ok((_, None)) => { let msg = format!("node {uid} has no thumbnail of the requested type"); - results.push(FileThumbnail::err( - uid, - ProtonError::invalid_operation(msg), - )); + results.push(FileThumbnail::err(uid, ProtonError::invalid_operation(msg))); } Err(e) => results.push(FileThumbnail::err(uid, e)), } @@ -695,9 +694,9 @@ impl ProtonDriveClient { .get_storage_blob(&block.bare_url, &block.token) .await { - Ok(ciphertext) => { - content_key.decrypt_block(&ciphertext).map_err(ProtonError::from) - } + Ok(ciphertext) => content_key + .decrypt_block(&ciphertext) + .map_err(ProtonError::from), Err(e) => Err(e), }; results.push(match downloaded { @@ -743,7 +742,11 @@ impl ProtonDriveClient { for_photos: bool, ) -> Result<(ContentKey, Option)> { let details = self - .get_link_details_ctx(&uid.volume_id, std::slice::from_ref(&uid.link_id), for_photos) + .get_link_details_ctx( + &uid.volume_id, + std::slice::from_ref(&uid.link_id), + for_photos, + ) .await?; let detail = details .links @@ -758,9 +761,9 @@ impl ProtonDriveClient { let content_key_packet_b64 = file.content_key_packet.ok_or_else(|| { ProtonError::invalid_operation("file is missing its content key packet") })?; - let content_key_packet = BASE64 - .decode(content_key_packet_b64.trim()) - .map_err(|e| ProtonError::invalid_operation(format!("decode content key packet: {e}")))?; + let content_key_packet = BASE64.decode(content_key_packet_b64.trim()).map_err(|e| { + ProtonError::invalid_operation(format!("decode content key packet: {e}")) + })?; let revision_id = file .active_revision .map(|r| r.id) @@ -920,7 +923,9 @@ impl ProtonDriveClient { let mut timer = self.telemetry.start("upload_photo"); timer.attr("aead", aead); if !self.ensure_photos().await? { - return Err(ProtonError::invalid_operation("account has no photos volume")); + return Err(ProtonError::invalid_operation( + "account has no photos volume", + )); } let parent_uid = self .cache @@ -938,8 +943,13 @@ impl ProtonDriveClient { let written = self.write_blocks(&draft, reader, thumbnails).await?; timer.attr("size", written.total_size); let photos_attributes = build_photos_attributes(&draft.parent_hash_key, &written, metadata); - self.seal_revision(&draft, &written, metadata.capture_time, Some(photos_attributes)) - .await?; + self.seal_revision( + &draft, + &written, + metadata.capture_time, + Some(photos_attributes), + ) + .await?; timer.success(); Ok(file_uid) @@ -971,7 +981,8 @@ impl ProtonDriveClient { let node = generate_node_key()?; let node_hash_key = generate_node_hash_key(&node.key)?; - let encrypted_name = parent_key.encrypt_and_sign(&signing_key, name.as_bytes(), true, false)?; + let encrypted_name = + parent_key.encrypt_and_sign(&signing_key, name.as_bytes(), true, false)?; let name_hash = hex::encode(hmac_sha256(&parent_hash_key, name.as_bytes())); let encrypted_passphrase = parent_key.encrypt(&node.passphrase)?; let passphrase_signature = signing_key.sign_detached(&node.passphrase)?; @@ -992,7 +1003,10 @@ impl ProtonDriveClient { let xattr_json = serde_json::to_vec(&xattr).map_err(|e| { ProtonError::invalid_operation(format!("serialize folder xattr: {e}")) })?; - Some(node.key.encrypt_and_sign(&signing_key, &xattr_json, false, true)?) + Some( + node.key + .encrypt_and_sign(&signing_key, &xattr_json, false, true)?, + ) } None => None, }; @@ -1230,9 +1244,9 @@ impl ProtonDriveClient { for chunk in uids.chunks(MAX_BATCH_COUNT) { let mut items = Vec::with_capacity(chunk.len()); for uid in chunk { - let link = links - .get(&uid.link_id) - .ok_or_else(|| ProtonError::invalid_operation(format!("node {uid} not found")))?; + let link = links.get(&uid.link_id).ok_or_else(|| { + ProtonError::invalid_operation(format!("node {uid} not found")) + })?; let parts = self .build_move_parts(uid, link, &dest_parent_key, &dest_hash_key, &signing_key) .await?; @@ -1403,7 +1417,8 @@ impl ProtonDriveClient { ContentKey::generate() }; - let encrypted_name = parent_key.encrypt_and_sign(&signing_key, name.as_bytes(), true, false)?; + let encrypted_name = + parent_key.encrypt_and_sign(&signing_key, name.as_bytes(), true, false)?; let name_hash = hex::encode(hmac_sha256(&parent_hash_key, name.as_bytes())); let encrypted_passphrase = parent_key.encrypt(&node.passphrase)?; let passphrase_signature = signing_key.sign_detached(&node.passphrase)?; @@ -1455,22 +1470,21 @@ impl ProtonDriveClient { let details = self .get_link_details(&volume_id, std::slice::from_ref(&file_uid.link_id)) .await?; - let detail = details - .links - .into_iter() - .next() - .ok_or_else(|| ProtonError::invalid_operation(format!("file {file_uid} not found")))?; + let detail = + details.links.into_iter().next().ok_or_else(|| { + ProtonError::invalid_operation(format!("file {file_uid} not found")) + })?; let link = detail.link; - let file = detail - .file - .ok_or_else(|| ProtonError::invalid_operation(format!("node {file_uid} is not a file")))?; + let file = detail.file.ok_or_else(|| { + ProtonError::invalid_operation(format!("node {file_uid} is not a file")) + })?; let content_key_packet_b64 = file.content_key_packet.ok_or_else(|| { ProtonError::invalid_operation("file is missing its content key packet") })?; - let content_key_packet = BASE64 - .decode(content_key_packet_b64.trim()) - .map_err(|e| ProtonError::invalid_operation(format!("decode content key packet: {e}")))?; + let content_key_packet = BASE64.decode(content_key_packet_b64.trim()).map_err(|e| { + ProtonError::invalid_operation(format!("decode content key packet: {e}")) + })?; let active_revision_id = file .active_revision .map(|r| r.id) @@ -1488,7 +1502,10 @@ impl ProtonDriveClient { client_uid: Some(self.http.session_id().to_string()), intended_upload_size, }; - let path = format!("v2/volumes/{volume_id}/files/{}/revisions", file_uid.link_id); + let path = format!( + "v2/volumes/{volume_id}/files/{}/revisions", + file_uid.link_id + ); let created: RevisionCreationResponse = self.http.post(&path, &request).await?; Ok(RevisionDraft { @@ -1674,7 +1691,9 @@ impl ProtonDriveClient { let xattr_json = serde_json::to_vec(&extended_attributes) .map_err(|e| ProtonError::invalid_operation(format!("serialize xattr: {e}")))?; let encrypted_xattr = - draft.node_key.encrypt_and_sign(&draft.signing_key, &xattr_json, false, true)?; + draft + .node_key + .encrypt_and_sign(&draft.signing_key, &xattr_json, false, true)?; let seal_request = RevisionUpdateRequest { manifest_signature, @@ -1712,7 +1731,9 @@ impl ProtonDriveClient { /// photos share's `MembershipAddressId` is cached by [`ensure_photos`]. async fn photos_membership_address(&self) -> Result<(AddressId, String, PrivateKey)> { if !self.ensure_photos().await? { - return Err(ProtonError::invalid_operation("account has no photos volume")); + return Err(ProtonError::invalid_operation( + "account has no photos volume", + )); } let address_id = self .cache @@ -1743,7 +1764,9 @@ impl ProtonDriveClient { let signing_key = keys .get(address.primary_key_index) .cloned() - .ok_or_else(|| ProtonError::invalid_operation("membership address has no primary key"))?; + .ok_or_else(|| { + ProtonError::invalid_operation("membership address has no primary key") + })?; Ok((address_id, address.email, signing_key)) } @@ -1754,7 +1777,8 @@ impl ProtonDriveClient { parent_uid: &NodeUid, parent_key: &PrivateKey, ) -> Result> { - self.parent_hash_key_ctx(parent_uid, parent_key, false).await + self.parent_hash_key_ctx(parent_uid, parent_key, false) + .await } /// As [`parent_hash_key`](Self::parent_hash_key), but routes the link-details @@ -1796,7 +1820,9 @@ impl ProtonDriveClient { let packet = BASE64 .decode(response.content_key_packet.trim()) - .map_err(|e| ProtonError::invalid_operation(format!("decode verification packet: {e}")))?; + .map_err(|e| { + ProtonError::invalid_operation(format!("decode verification packet: {e}")) + })?; node_key.decrypt_content_key(&packet).map_err(|e| { ProtonError::invalid_operation(format!("verification content key mismatch: {e}")) })?; @@ -1883,9 +1909,7 @@ impl ProtonDriveClient { let mut cache = self.cache.lock().await; cache.main_volume_id = Some(volume_id); cache.my_files_root = Some(root_uid.clone()); - cache - .folder_keys - .insert(root_uid, decrypted_root.node_key); + cache.folder_keys.insert(root_uid, decrypted_root.node_key); cache.my_files_share = Some(ShareKey { share_id, address_id: response.share.address_id.clone(), @@ -2156,9 +2180,9 @@ impl ProtonDriveClient { let content_key_packet_b64 = file.content_key_packet.ok_or_else(|| { ProtonError::invalid_operation("photo is missing its content key packet") })?; - let content_key_packet = BASE64 - .decode(content_key_packet_b64.trim()) - .map_err(|e| ProtonError::invalid_operation(format!("decode content key packet: {e}")))?; + let content_key_packet = BASE64.decode(content_key_packet_b64.trim()).map_err(|e| { + ProtonError::invalid_operation(format!("decode content key packet: {e}")) + })?; let revision_id = file .active_revision .map(|r| r.id) @@ -2635,7 +2659,7 @@ fn to_drive_event(volume_id: &VolumeId, event: &VolumeEventDto) -> Result Ok(DriveEvent::NodeUpdated { + 1..=3 => Ok(DriveEvent::NodeUpdated { id: event.id.clone(), node_uid, parent_node_uid, @@ -2848,7 +2872,10 @@ mod tests { }; let metadata = PhotoUploadMetadata { capture_time: Some(1_700_000_000), - main_photo_uid: Some(NodeUid::new(VolumeId::new("vol-1"), LinkId::new("main-link"))), + main_photo_uid: Some(NodeUid::new( + VolumeId::new("vol-1"), + LinkId::new("main-link"), + )), tags: vec![PhotoTag::Video, PhotoTag::Selfie], }; @@ -2860,7 +2887,10 @@ mod tests { assert_eq!(attrs.capture_time, 1_700_000_000); assert_eq!(attrs.main_photo_link_id, Some(LinkId::new("main-link"))); // Tags carry their `PhotoTag` discriminants. - assert_eq!(attrs.tags, vec![PhotoTag::Video as i32, PhotoTag::Selfie as i32]); + assert_eq!( + attrs.tags, + vec![PhotoTag::Video as i32, PhotoTag::Selfie as i32] + ); } #[test] diff --git a/crates/proton-drive-sdk/src/crypto.rs b/crates/proton-drive-sdk/src/crypto.rs index d098c24..f64d820 100644 --- a/crates/proton-drive-sdk/src/crypto.rs +++ b/crates/proton-drive-sdk/src/crypto.rs @@ -44,8 +44,9 @@ pub fn decrypt_link(parent_key: &PrivateKey, link: &LinkDto) -> Result, #[serde(rename = "Name")] pub name: String, @@ -550,7 +553,10 @@ pub struct MoveMultipleLinksItem { pub name_hash: String, #[serde(rename = "OriginalHash")] pub original_hash: String, - #[serde(rename = "NodePassphraseSignature", skip_serializing_if = "Option::is_none")] + #[serde( + rename = "NodePassphraseSignature", + skip_serializing_if = "Option::is_none" + )] pub passphrase_signature: Option, } diff --git a/crates/proton-drive-sdk/src/events.rs b/crates/proton-drive-sdk/src/events.rs index 09f57fb..b305872 100644 --- a/crates/proton-drive-sdk/src/events.rs +++ b/crates/proton-drive-sdk/src/events.rs @@ -111,13 +111,7 @@ mod tests { #[test] fn id_covers_parity_variants() { let id = DriveEventId::new("evt-1"); - assert_eq!( - DriveEvent::ScopeAccessLost { id: id.clone() }.id(), - &id - ); - assert_eq!( - DriveEvent::SharedWithMeUpdated { id: id.clone() }.id(), - &id - ); + assert_eq!(DriveEvent::ScopeAccessLost { id: id.clone() }.id(), &id); + assert_eq!(DriveEvent::SharedWithMeUpdated { id: id.clone() }.id(), &id); } } diff --git a/crates/proton-drive-sdk/src/lib.rs b/crates/proton-drive-sdk/src/lib.rs index 7328d9c..09a9c42 100644 --- a/crates/proton-drive-sdk/src/lib.rs +++ b/crates/proton-drive-sdk/src/lib.rs @@ -43,10 +43,8 @@ mod photos; pub use cache::{CachedNodeInfo, DriveEntityCache}; pub use client::ProtonDriveClient; pub use events::{DriveEvent, DriveEventScopeId}; -pub use photos::{PhotoTag, PhotosTimelineItem, ProtonPhotosClient}; -pub use proton_sdk::cache::{ - CacheRepository, EncryptedCacheRepository, InMemoryCacheRepository, -}; pub use node::{Node, NodeKind, Thumbnail, ThumbnailType}; +pub use photos::{PhotoTag, PhotosTimelineItem, ProtonPhotosClient}; +pub use proton_sdk::cache::{CacheRepository, EncryptedCacheRepository, InMemoryCacheRepository}; pub use proton_sdk; diff --git a/crates/proton-drive-sdk/src/node.rs b/crates/proton-drive-sdk/src/node.rs index 0e8906c..06b3ec0 100644 --- a/crates/proton-drive-sdk/src/node.rs +++ b/crates/proton-drive-sdk/src/node.rs @@ -60,8 +60,9 @@ impl NodeVerification { /// `NotSigned` is treated as acceptable (Proton metadata is not always /// signed); only `NoVerifier`/`Failed` count against trust. pub fn is_fully_verified(&self) -> bool { - let ok = - |s: VerificationStatus| matches!(s, VerificationStatus::Ok | VerificationStatus::NotSigned); + let ok = |s: VerificationStatus| { + matches!(s, VerificationStatus::Ok | VerificationStatus::NotSigned) + }; ok(self.name) && ok(self.passphrase) && self.content_key.map_or(true, ok) diff --git a/crates/proton-sdk/src/account.rs b/crates/proton-sdk/src/account.rs index b7ca3d8..b192f4f 100644 --- a/crates/proton-sdk/src/account.rs +++ b/crates/proton-sdk/src/account.rs @@ -130,13 +130,9 @@ impl AccountClient { self.addresses().await?; let cache = self.inner.cache.lock().await; - cache - .address_keys - .get(address_id) - .cloned() - .ok_or_else(|| { - ProtonError::invalid_operation(format!("no keys for address {address_id}")) - }) + cache.address_keys.get(address_id).cloned().ok_or_else(|| { + ProtonError::invalid_operation(format!("no keys for address {address_id}")) + }) } /// Active public keys for an email address, used to verify authorship @@ -217,7 +213,9 @@ impl AccountClient { }; match PrivateKey::from_armored(&key_dto.private_key, passphrase) { Ok(key) => keys.push(key), - Err(e) => tracing::warn!(key_id = %key_dto.id, error = %e, "failed to unlock user key"), + Err(e) => { + tracing::warn!(key_id = %key_dto.id, error = %e, "failed to unlock user key") + } } } @@ -244,7 +242,9 @@ impl AccountClient { let response: KeySaltListResponse = self.inner.http.get("core/v4/keys/salts").await?; let mut passphrases = HashMap::new(); for salt in &response.key_salts { - let Some(salt_b64) = &salt.value else { continue }; + let Some(salt_b64) = &salt.value else { + continue; + }; if salt_b64.is_empty() { continue; } @@ -308,10 +308,12 @@ impl AccountClient { } } - let primary_key_index = primary_key_index - .ok_or_else(|| ProtonError::invalid_operation(format!("address {} has no primary key", dto.id)))?; - let primary_key_id = primary_key_id - .ok_or_else(|| ProtonError::invalid_operation(format!("address {} has no primary key", dto.id)))?; + let primary_key_index = primary_key_index.ok_or_else(|| { + ProtonError::invalid_operation(format!("address {} has no primary key", dto.id)) + })?; + let primary_key_id = primary_key_id.ok_or_else(|| { + ProtonError::invalid_operation(format!("address {} has no primary key", dto.id)) + })?; let address = Address { id: dto.id.clone(), diff --git a/crates/proton-sdk/src/cache.rs b/crates/proton-sdk/src/cache.rs index 9f3b308..8a2a44a 100644 --- a/crates/proton-sdk/src/cache.rs +++ b/crates/proton-sdk/src/cache.rs @@ -392,7 +392,10 @@ mod tests { async fn encrypted_round_trips_and_hides_plaintext() { let inner = InMemoryCacheRepository::shared(); let cache = EncryptedCacheRepository::new(inner.clone(), b"hunter2-master-key".to_vec()); - cache.set("share:1", "secret-value", &tags(&["t"])).await.unwrap(); + cache + .set("share:1", "secret-value", &tags(&["t"])) + .await + .unwrap(); // Stored ciphertext is not the plaintext. let stored = inner.get("share:1").await.unwrap().unwrap(); @@ -405,7 +408,10 @@ mod tests { ); // Tags pass through to the inner store. let by_tag = cache.get_by_tags(&tags(&["t"])).await.unwrap(); - assert_eq!(by_tag, vec![("share:1".to_string(), "secret-value".to_string())]); + assert_eq!( + by_tag, + vec![("share:1".to_string(), "secret-value".to_string())] + ); } #[tokio::test] diff --git a/crates/proton-sdk/src/crypto/content.rs b/crates/proton-sdk/src/crypto/content.rs index 4ec46e3..37a6c5a 100644 --- a/crates/proton-sdk/src/crypto/content.rs +++ b/crates/proton-sdk/src/crypto/content.rs @@ -62,7 +62,9 @@ impl ContentKey { /// `PgpSessionKey.GenerateForAead()`. pub fn generate_aead() -> Self { let mut rng = rand::thread_rng(); - let key = SymmetricKeyAlgorithm::AES256.new_session_key(&mut rng).to_vec(); + let key = SymmetricKeyAlgorithm::AES256 + .new_session_key(&mut rng) + .to_vec(); ContentKey { session_key: PlainSessionKey::V6 { key }, } @@ -113,7 +115,11 @@ impl ContentKey { // session key, so we leave the builder unencrypted and seal the bytes // ourselves with our content key below. let mut builder = MessageBuilder::from_bytes(bytes::Bytes::new(), plaintext.to_vec()); - builder.sign(&signer.key().primary_key, signer.password(), HashAlgorithm::Sha256); + builder.sign( + &signer.key().primary_key, + signer.password(), + HashAlgorithm::Sha256, + ); let inner = builder .to_vec(&mut rng) .map_err(|e| CryptoError::Encrypt(format!("sign thumbnail: {e}")))?; @@ -405,9 +411,7 @@ impl RecipientOp for ContentKeyPacketV6Op<'_> { #[cfg(test)] mod tests { use super::*; - use pgp::composed::{ - KeyType, MessageBuilder, SecretKeyParamsBuilder, SignedSecretKey, - }; + use pgp::composed::{KeyType, MessageBuilder, SecretKeyParamsBuilder, SignedSecretKey}; use pgp::crypto::sym::SymmetricKeyAlgorithm; /// Generate a passphrase-locked RSA key able to both sign and encrypt — a @@ -471,14 +475,18 @@ mod tests { let node_key = PrivateKey::from_armored(&armored, b"pw").expect("parse node key"); let content_key = ContentKey::generate(); - let packet = content_key.to_packet(&node_key).expect("content key packet"); + let packet = content_key + .to_packet(&node_key) + .expect("content key packet"); let recovered = node_key .decrypt_content_key(&packet) .expect("decrypt content key"); let plaintext = b"upload round trip block".to_vec(); - let block = content_key.encrypt_block(&plaintext).expect("encrypt block"); + let block = content_key + .encrypt_block(&plaintext) + .expect("encrypt block"); let decrypted = recovered.decrypt_block(&block).expect("decrypt block"); assert_eq!(decrypted, plaintext); @@ -492,11 +500,15 @@ mod tests { let node = super::super::encrypt::generate_node_key().expect("generate node key"); let content_key = ContentKey::generate(); - let packet = content_key.to_packet(&node.key).expect("content key packet"); + let packet = content_key + .to_packet(&node.key) + .expect("content key packet"); let recovered = node.key.decrypt_content_key(&packet).expect("decrypt"); let plaintext = b"x25519 block".to_vec(); - let block = content_key.encrypt_block(&plaintext).expect("encrypt block"); + let block = content_key + .encrypt_block(&plaintext) + .expect("encrypt block"); assert_eq!(recovered.decrypt_block(&block).expect("decrypt"), plaintext); } @@ -522,7 +534,9 @@ mod tests { Packet::SymEncryptedProtectedData(d) => d, other => panic!("expected SEIPD, got {:?}", other.tag()), }; - let inner = seipd.decrypt(key, Some(sym_alg)).expect("decrypt thumbnail"); + let inner = seipd + .decrypt(key, Some(sym_alg)) + .expect("decrypt thumbnail"); let mut inner_parser = PacketParser::new(Cursor::new(&inner)); let first = inner_parser.next().expect("inner packet").expect("inner"); @@ -532,7 +546,10 @@ mod tests { first.tag() ); // The literal data follows the one-pass signature. - let literal = inner_parser.next().expect("literal packet").expect("literal"); + let literal = inner_parser + .next() + .expect("literal packet") + .expect("literal"); match literal { Packet::LiteralData(data) => assert_eq!(data.data(), plaintext.as_slice()), other => panic!("expected literal data, got {:?}", other.tag()), @@ -548,13 +565,17 @@ mod tests { let content_key = ContentKey::generate_aead(); assert!(content_key.is_aead()); - let packet = content_key.to_packet(&node.key).expect("content key packet"); + let packet = content_key + .to_packet(&node.key) + .expect("content key packet"); let recovered = node.key.decrypt_content_key(&packet).expect("decrypt"); assert!(recovered.is_aead(), "recovered key should be AEAD"); // A block larger than one AEAD chunk (128 KiB) exercises chunked GCM. let plaintext = vec![0xABu8; (1 << 17) + 1234]; - let block = content_key.encrypt_block(&plaintext).expect("encrypt block"); + let block = content_key + .encrypt_block(&plaintext) + .expect("encrypt block"); assert_eq!( recovered.decrypt_block(&block).expect("decrypt block"), plaintext @@ -577,7 +598,10 @@ mod tests { .encrypt_and_sign(&signer.key, &passphrase, false, false) .expect("encrypt passphrase to old parent"); // The move keeps the detached NodePassphraseSignature, so sign separately. - let signature = signer.key.sign_detached(&passphrase).expect("sign passphrase"); + let signature = signer + .key + .sign_detached(&passphrase) + .expect("sign passphrase"); // The new parent cannot read the old-parent message yet. assert!(new_parent.key.decrypt_armored_message(&armored).is_err()); @@ -603,7 +627,9 @@ mod tests { fn rejects_non_pkesk_content_key_packet() { let mut rng = rand::thread_rng(); let key = PrivateKey::from_armored( - &generate_node_key("pw").to_armored_string(None.into()).unwrap(), + &generate_node_key("pw") + .to_armored_string(None.into()) + .unwrap(), b"pw", ) .unwrap(); diff --git a/crates/proton-sdk/src/crypto/encrypt.rs b/crates/proton-sdk/src/crypto/encrypt.rs index ff3aaa4..509ea24 100644 --- a/crates/proton-sdk/src/crypto/encrypt.rs +++ b/crates/proton-sdk/src/crypto/encrypt.rs @@ -9,6 +9,7 @@ use base64::engine::general_purpose::STANDARD as BASE64; use base64::Engine; use bytes::Bytes; +use chrono::SubsecRound; use pgp::composed::{ ArmorOptions, KeyType, MessageBuilder, SecretKeyParamsBuilder, SignedSecretKey, StandaloneSignature, SubkeyParamsBuilder, @@ -17,7 +18,6 @@ use pgp::crypto::hash::HashAlgorithm; use pgp::crypto::sym::SymmetricKeyAlgorithm; use pgp::packet::{SignatureConfig, SignatureType, Subpacket, SubpacketData}; use pgp::types::{CompressionAlgorithm, KeyDetails, KeyVersion, Password, PublicKeyTrait}; -use chrono::SubsecRound; use rand::RngCore; use super::errors::CryptoError; @@ -193,9 +193,10 @@ pub fn build_volume_creation_material( let share_passphrase = address_key.encrypt(&share.passphrase)?; let share_passphrase_signature = address_key.sign_detached(&share.passphrase)?; - let folder_name = share - .key - .encrypt_and_sign(address_key, root_folder_name.as_bytes(), true, false)?; + let folder_name = + share + .key + .encrypt_and_sign(address_key, root_folder_name.as_bytes(), true, false)?; let folder_passphrase = share.key.encrypt(&folder.passphrase)?; let folder_passphrase_signature = address_key.sign_detached(&folder.passphrase)?; @@ -327,10 +328,7 @@ mod tests { .key .encrypt_and_sign(&signer.key, &plaintext, false, true) .expect("encrypt and sign"); - let decrypted = node - .key - .decrypt_armored_message(&armored) - .expect("decrypt"); + let decrypted = node.key.decrypt_armored_message(&armored).expect("decrypt"); assert_eq!(decrypted, plaintext); // Encrypt-only (no signature) must also round-trip. diff --git a/crates/proton-sdk/src/crypto/keys.rs b/crates/proton-sdk/src/crypto/keys.rs index d9add88..da9f6d1 100644 --- a/crates/proton-sdk/src/crypto/keys.rs +++ b/crates/proton-sdk/src/crypto/keys.rs @@ -69,7 +69,11 @@ impl PrivateKey { /// Verify an armored detached signature over `data` against this key's /// public key. Returns `Ok(())` only on a good signature. - pub fn verify_detached_signature(&self, armored_sig: &str, data: &[u8]) -> Result<(), CryptoError> { + pub fn verify_detached_signature( + &self, + armored_sig: &str, + data: &[u8], + ) -> Result<(), CryptoError> { let (sig, _headers) = StandaloneSignature::from_string(armored_sig) .map_err(|e| CryptoError::Parse(format!("detached signature: {e}")))?; sig.verify(&self.key.signed_public_key(), data) @@ -110,11 +114,13 @@ impl PrivateKey { } /// Decrypt an armored message, trying each key in `keys` until one succeeds. -pub fn decrypt_armored_with_keys(armored: &str, keys: &[PrivateKey]) -> Result, CryptoError> { +pub fn decrypt_armored_with_keys( + armored: &str, + keys: &[PrivateKey], +) -> Result, CryptoError> { let owned: Vec<(&SignedSecretKey, Password)> = keys.iter().map(|k| (k.key(), k.password())).collect(); - let refs: Vec<(&SignedSecretKey, &Password)> = - owned.iter().map(|(k, p)| (*k, p)).collect(); + let refs: Vec<(&SignedSecretKey, &Password)> = owned.iter().map(|(k, p)| (*k, p)).collect(); messages::decrypt_armored_any(armored, &refs) } diff --git a/crates/proton-sdk/src/crypto/messages.rs b/crates/proton-sdk/src/crypto/messages.rs index e884a98..9bc18e5 100644 --- a/crates/proton-sdk/src/crypto/messages.rs +++ b/crates/proton-sdk/src/crypto/messages.rs @@ -15,8 +15,8 @@ pub fn decrypt_armored( key: &SignedSecretKey, key_pw: &Password, ) -> Result, CryptoError> { - let (message, _headers) = Message::from_string(armored) - .map_err(|e| CryptoError::Parse(format!("message: {e}")))?; + let (message, _headers) = + Message::from_string(armored).map_err(|e| CryptoError::Parse(format!("message: {e}")))?; let decrypted = message .decrypt(key_pw, key) @@ -31,8 +31,8 @@ pub fn decrypt_binary( key: &SignedSecretKey, key_pw: &Password, ) -> Result, CryptoError> { - let message = Message::from_bytes(ciphertext) - .map_err(|e| CryptoError::Parse(format!("message: {e}")))?; + let message = + Message::from_bytes(ciphertext).map_err(|e| CryptoError::Parse(format!("message: {e}")))?; let decrypted = message .decrypt(key_pw, key) diff --git a/crates/proton-sdk/src/crypto/srp.rs b/crates/proton-sdk/src/crypto/srp.rs index 4e92a57..80bb1a4 100644 --- a/crates/proton-sdk/src/crypto/srp.rs +++ b/crates/proton-sdk/src/crypto/srp.rs @@ -80,11 +80,7 @@ fn expand_hash(data: &[u8]) -> Vec { /// Hash the password for auth versions 3/4: `expandHash(bcrypt(pw, salt+"proton") || modulus)`. /// /// `salt` is the raw (decoded) login salt; `modulus` is the raw modulus bytes. -fn hash_password_v3( - password: &[u8], - salt: &[u8], - modulus: &[u8], -) -> Result, CryptoError> { +fn hash_password_v3(password: &[u8], salt: &[u8], modulus: &[u8]) -> Result, CryptoError> { // Proton appends the literal "proton" to the 10-byte salt, yielding bcrypt's // required 16-byte salt. let mut salt_buf = [0u8; 16]; @@ -139,11 +135,15 @@ fn to_le_fixed(n: &BigUint, byte_len: usize) -> Result, CryptoError> { /// modulus signature has already been verified against Proton's key. fn check_params(bit_length: usize, n: &BigUint, b: &BigUint) -> Result<(), CryptoError> { if n.bits() as usize != bit_length { - return Err(CryptoError::Verification("SRP modulus has wrong size".into())); + return Err(CryptoError::Verification( + "SRP modulus has wrong size".into(), + )); } // 2 generates the whole group only when N ≡ 3 (mod 8). if (n % 8u32) != BigUint::from(3u32) { - return Err(CryptoError::Verification("SRP modulus is not 3 mod 8".into())); + return Err(CryptoError::Verification( + "SRP modulus is not 3 mod 8".into(), + )); } let n_minus_1 = n - 1u32; if *b <= BigUint::one() || *b >= n_minus_1 { @@ -174,8 +174,7 @@ fn generate_ephemeral( loop { let mut buf = vec![0u8; byte_len]; - getrandom::getrandom(&mut buf) - .map_err(|e| CryptoError::Decrypt(format!("rng: {e}")))?; + getrandom::getrandom(&mut buf).map_err(|e| CryptoError::Decrypt(format!("rng: {e}")))?; let secret = from_le(&buf) % n_minus_1; if secret <= lower_bound || secret >= *n_minus_1 { continue; @@ -233,7 +232,9 @@ pub fn generate_proofs( k_input.extend_from_slice(&to_le_fixed(&n, byte_len)?); let k = from_le(&expand_hash(&k_input)) % &n; if k <= BigUint::one() || k >= n_minus_1 { - return Err(CryptoError::Verification("SRP multiplier out of bounds".into())); + return Err(CryptoError::Verification( + "SRP multiplier out of bounds".into(), + )); } let (secret, a_bytes, scrambling) = diff --git a/crates/proton-sdk/src/crypto/verify.rs b/crates/proton-sdk/src/crypto/verify.rs index 558a9ad..e49202f 100644 --- a/crates/proton-sdk/src/crypto/verify.rs +++ b/crates/proton-sdk/src/crypto/verify.rs @@ -96,7 +96,10 @@ impl VerificationKeyRing { /// Used for content-key and hash-key signatures, where C# /// `GetContentKeyAndHashKeyVerificationKeyRing` always includes the node /// key alongside any claimed-author keys. - pub fn from_private_and_public_keys(key: &super::PrivateKey, public_keys: &[PublicKey]) -> Self { + pub fn from_private_and_public_keys( + key: &super::PrivateKey, + public_keys: &[PublicKey], + ) -> Self { let mut keys = Vec::with_capacity(public_keys.len() + 1); keys.push(key.signed_public_key()); keys.extend(public_keys.iter().map(|k| k.signed().clone())); @@ -114,7 +117,11 @@ impl VerificationKeyRing { } /// Verify an armored detached signature over `data` against `ring`. -pub fn verify_detached(armored_sig: &str, data: &[u8], ring: &VerificationKeyRing) -> VerificationStatus { +pub fn verify_detached( + armored_sig: &str, + data: &[u8], + ring: &VerificationKeyRing, +) -> VerificationStatus { let signature = match StandaloneSignature::from_string(armored_sig) { Ok((sig, _headers)) => sig, Err(_) => return VerificationStatus::Failed, @@ -206,7 +213,10 @@ mod tests { let sig = signer.key.sign_detached(data).expect("sign"); let ring = VerificationKeyRing::from_private(&other.key); - assert_eq!(verify_detached(&sig, data, &ring), VerificationStatus::Failed); + assert_eq!( + verify_detached(&sig, data, &ring), + VerificationStatus::Failed + ); } #[test] diff --git a/crates/proton-sdk/src/http.rs b/crates/proton-sdk/src/http.rs index 476a683..fb3ba19 100644 --- a/crates/proton-sdk/src/http.rs +++ b/crates/proton-sdk/src/http.rs @@ -111,7 +111,11 @@ impl ApiHttpClient { /// previous sink. Takes effect for every clone of this client, since they /// share state. pub fn set_telemetry(&self, telemetry: Arc) { - *self.inner.telemetry.lock().expect("telemetry mutex poisoned") = telemetry; + *self + .inner + .telemetry + .lock() + .expect("telemetry mutex poisoned") = telemetry; } /// Snapshot the current telemetry sink. @@ -283,7 +287,8 @@ impl ApiHttpClient { } let access_token = self.refresh_access_token(&rejected_access_token).await?; - self.send_with_token(method, path, body, &access_token).await + self.send_with_token(method, path, body, &access_token) + .await } async fn send_with_token( @@ -448,9 +453,12 @@ fn api_error(status: StatusCode, bytes: &[u8]) -> ProtonError { .as_ref() .map(|e| e.code) .unwrap_or(ResponseCode::Unknown); - let message = envelope - .and_then(|e| e.error_message) - .unwrap_or_else(|| status.canonical_reason().unwrap_or("unknown error").to_owned()); + let message = envelope.and_then(|e| e.error_message).unwrap_or_else(|| { + status + .canonical_reason() + .unwrap_or("unknown error") + .to_owned() + }); ProtonError::Api(ProtonApiError { code, @@ -567,10 +575,16 @@ mod tests { #[test] fn retry_after_parses_seconds_only() { assert_eq!(parse_retry_after_secs("5"), Some(Duration::from_secs(5))); - assert_eq!(parse_retry_after_secs(" 12 "), Some(Duration::from_secs(12))); + assert_eq!( + parse_retry_after_secs(" 12 "), + Some(Duration::from_secs(12)) + ); assert_eq!(parse_retry_after_secs("0"), Some(Duration::ZERO)); // HTTP-date form is unsupported -> falls back to backoff. - assert_eq!(parse_retry_after_secs("Wed, 21 Oct 2015 07:28:00 GMT"), None); + assert_eq!( + parse_retry_after_secs("Wed, 21 Oct 2015 07:28:00 GMT"), + None + ); assert_eq!(parse_retry_after_secs(""), None); }