fmt + Clippy

This commit is contained in:
nvrl committed 2026-06-26 20:47:06 +02:00
1 parent ec590fd52b
commit e2f5ab7ad6
17 files changed
+258 -153

No files matched your search

@@ -54,13 +54,13 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
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 {
+15 -4
View File
@@ -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.
+93 -63
View File
@@ -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<Option<Vec<u8>>> {
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<String>)> {
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,7 +943,12 @@ 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))
self.seal_revision(
&draft,
&written,
metadata.capture_time,
Some(photos_attributes),
)
.await?;
timer.success();
@@ -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<Vec<u8>> {
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<DriveE
// VolumeEventType: 0 Delete, 1 Create, 2 Update, 3 UpdateMetadata.
match event.event_type {
1 | 2 | 3 => 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]
+6 -4
View File
@@ -44,8 +44,9 @@ pub fn decrypt_link(parent_key: &PrivateKey, link: &LinkDto) -> Result<Decrypted
let node_key = PrivateKey::from_armored(&link.key, &passphrase)?;
let name_bytes = parent_key.decrypt_armored_message(&link.name)?;
let name = String::from_utf8(name_bytes)
.map_err(|e| ProtonError::invalid_operation(format!("node name is not valid UTF-8: {e}")))?;
let name = String::from_utf8(name_bytes).map_err(|e| {
ProtonError::invalid_operation(format!("node name is not valid UTF-8: {e}"))
})?;
Ok(DecryptedLink { node_key, name })
}
@@ -125,8 +126,9 @@ pub async fn decrypt_link_verified(
// Name: an inline-signed message addressed to the parent key.
let (name_bytes, name_status) =
parent_key.decrypt_armored_verify(&link.name, &name_claim.ring(parent_key))?;
let name = String::from_utf8(name_bytes)
.map_err(|e| ProtonError::invalid_operation(format!("node name is not valid UTF-8: {e}")))?;
let name = String::from_utf8(name_bytes).map_err(|e| {
ProtonError::invalid_operation(format!("node name is not valid UTF-8: {e}"))
})?;
let verification = NodeVerification {
name: name_status,
+8 -2
View File
@@ -507,7 +507,10 @@ pub struct MoveLinkRequest {
pub parent_link_id: LinkId,
#[serde(rename = "NodePassphrase")]
pub passphrase: String,
#[serde(rename = "NodePassphraseSignature", skip_serializing_if = "Option::is_none")]
#[serde(
rename = "NodePassphraseSignature",
skip_serializing_if = "Option::is_none"
)]
pub passphrase_signature: Option<String>,
#[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<String>,
}
+2 -8
View File
@@ -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);
}
}
+2 -4
View File
@@ -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;
+3 -2
View File
@@ -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)
+13 -11
View File
@@ -130,11 +130,7 @@ impl AccountClient {
self.addresses().await?;
let cache = self.inner.cache.lock().await;
cache
.address_keys
.get(address_id)
.cloned()
.ok_or_else(|| {
cache.address_keys.get(address_id).cloned().ok_or_else(|| {
ProtonError::invalid_operation(format!("no keys for address {address_id}"))
})
}
@@ -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(),
+8 -2
View File
@@ -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]
+41 -15
View File
@@ -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();
+4 -6
View File
@@ -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,7 +193,8 @@ 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
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)?;
@@ -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.
+10 -4
View File
@@ -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<Vec<u8>, CryptoError> {
pub fn decrypt_armored_with_keys(
armored: &str,
keys: &[PrivateKey],
) -> Result<Vec<u8>, 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)
}
+4 -4
View File
@@ -15,8 +15,8 @@ pub fn decrypt_armored(
key: &SignedSecretKey,
key_pw: &Password,
) -> Result<Vec<u8>, 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<Vec<u8>, 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)
+11 -10
View File
@@ -80,11 +80,7 @@ fn expand_hash(data: &[u8]) -> Vec<u8> {
/// 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<Vec<u8>, CryptoError> {
fn hash_password_v3(password: &[u8], salt: &[u8], modulus: &[u8]) -> Result<Vec<u8>, 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<Vec<u8>, 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) =
+13 -3
View File
@@ -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]
+21 -7
View File
@@ -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<dyn Telemetry>) {
*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<B: Serialize>(
@@ -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);
}