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