mirror of
https://github.com/narrrl/proton-sdk-rs.git
synced 2026-11-03 16:41:26 +01:00
feat(drive,sdk): port upstream C# SDK up to ce000edd
- Moves go through `move-multiple` in batches of 150, with an optional target name per node (`move_nodes_streaming` / `NodeMoveItem`), a single `InvalidRequirements` retry against the server's current name hash, and passphrase signing for anonymously created nodes. A batch rejected as a whole keeps its API error code on every node. - Share memberships carry invite time and inviter email, and the inviter's `drive.share-member.inviter` signature is verified as non-fatal metadata (`verify_detached_with_context`). - `Node` gains `direct_role` and `share_id`. - Trashed detection reads `TrashTime` as well as the legacy `Trashed`. Breaking: new public struct fields on `Node` and `ShareMembership`.
This commit is contained in:
1 parent
81844cb9da
commit
d72065418b
11 files changed
+667
-198
No files matched your search
@@ -26,7 +26,10 @@ pub use errors::CryptoError;
|
||||
pub use keys::{PrivateKey, decrypt_armored_with_keys};
|
||||
pub use messages::decrypt_armored_with_password;
|
||||
pub use srp::{DEFAULT_BIT_LENGTH, SrpProofs, SrpVerifier, generate_proofs, generate_verifier};
|
||||
pub use verify::{PublicKey, VerificationKeyRing, VerificationStatus, verify_detached};
|
||||
pub use verify::{
|
||||
PublicKey, VerificationKeyRing, VerificationStatus, verify_detached,
|
||||
verify_detached_with_context,
|
||||
};
|
||||
|
||||
/// Result alias for crypto operations.
|
||||
pub type CryptoResult<T> = std::result::Result<T, CryptoError>;
|
||||
@@ -324,7 +324,7 @@ pub(crate) fn sign_detached(signer: &PrivateKey, data: &[u8]) -> Result<String,
|
||||
/// separation (`context@proton.ch`, critical). Proton signs sharing invitations,
|
||||
/// members and external invitations under distinct contexts so a signature made
|
||||
/// for one purpose cannot be replayed as another.
|
||||
const SIGNATURE_CONTEXT_NOTATION: &[u8] = b"context@proton.ch";
|
||||
pub(crate) const SIGNATURE_CONTEXT_NOTATION: &[u8] = b"context@proton.ch";
|
||||
|
||||
/// Like [`sign_detached`] but stamps a GopenPGP signature *context* — a critical
|
||||
/// `context@proton.ch` notation — into the hashed subpackets, and returns the
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
//! `NodeCrypto` records the `PgpVerificationStatus` of each decrypt rather than
|
||||
//! throwing.
|
||||
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose::STANDARD as BASE64;
|
||||
use pgp::composed::{
|
||||
Deserializable, DetachedSignature, Message, SignedPublicKey, SignedSecretKey,
|
||||
VerificationResult,
|
||||
@@ -15,6 +17,7 @@ use pgp::composed::{
|
||||
use pgp::types::{Password, VerifyingKey};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::encrypt::SIGNATURE_CONTEXT_NOTATION;
|
||||
use super::errors::CryptoError;
|
||||
|
||||
/// Outcome of an authorship/signature check.
|
||||
@@ -137,6 +140,48 @@ pub fn verify_detached(
|
||||
VerificationStatus::Failed
|
||||
}
|
||||
|
||||
/// Verify a detached signature made under a GopenPGP signature *context* (the
|
||||
/// `context@proton.ch` notation) — the counterpart of the sharing signers in
|
||||
/// `encrypt.rs`, and of C# `PgpVerificationContext`.
|
||||
///
|
||||
/// The signature may be armored or base64 of the binary packet: the sharing
|
||||
/// endpoints are not consistent about which they return. A signature that
|
||||
/// verifies but was made under another context — or none — is `Failed`, since
|
||||
/// accepting it would let a signature made for one purpose stand in for another.
|
||||
pub fn verify_detached_with_context(
|
||||
signature: &str,
|
||||
data: &[u8],
|
||||
ring: &VerificationKeyRing,
|
||||
context: &str,
|
||||
) -> VerificationStatus {
|
||||
if ring.is_empty() {
|
||||
return VerificationStatus::NoVerifier;
|
||||
}
|
||||
let parsed = DetachedSignature::from_string(signature)
|
||||
.map(|(signature, _headers)| signature)
|
||||
.ok()
|
||||
.or_else(|| {
|
||||
let bytes = BASE64.decode(signature.trim()).ok()?;
|
||||
DetachedSignature::from_bytes(&bytes[..]).ok()
|
||||
});
|
||||
let Some(signature) = parsed else {
|
||||
return VerificationStatus::Failed;
|
||||
};
|
||||
let in_context = signature.signature.notations().iter().any(|notation| {
|
||||
notation.name.as_ref() == SIGNATURE_CONTEXT_NOTATION
|
||||
&& notation.value.as_ref() == context.as_bytes()
|
||||
});
|
||||
if !in_context {
|
||||
return VerificationStatus::Failed;
|
||||
}
|
||||
for key in &ring.keys {
|
||||
if signature.verify(key, data).is_ok() {
|
||||
return VerificationStatus::Ok;
|
||||
}
|
||||
}
|
||||
VerificationStatus::Failed
|
||||
}
|
||||
|
||||
/// Decrypt an armored PGP message and verify its inline signature (if any)
|
||||
/// against `ring`. Always returns the plaintext; the status is metadata.
|
||||
pub(crate) fn decrypt_and_verify(
|
||||
@@ -243,6 +288,64 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn context_signature_verifies_only_under_its_own_context() {
|
||||
use crate::crypto::encrypt::sign_detached_binary_with_context;
|
||||
use crate::crypto::{SHARING_INVITER_CONTEXT, SHARING_MEMBER_CONTEXT};
|
||||
|
||||
let signer = generate_node_key().expect("generate signer key");
|
||||
let data = b"share passphrase key packet";
|
||||
let binary = sign_detached_binary_with_context(&signer.key, data, SHARING_INVITER_CONTEXT)
|
||||
.expect("sign");
|
||||
let base64 = BASE64.encode(&binary);
|
||||
let ring = VerificationKeyRing::from_private(&signer.key);
|
||||
|
||||
assert_eq!(
|
||||
verify_detached_with_context(&base64, data, &ring, SHARING_INVITER_CONTEXT),
|
||||
VerificationStatus::Ok
|
||||
);
|
||||
// The right key over the right bytes, made for a different purpose.
|
||||
assert_eq!(
|
||||
verify_detached_with_context(&base64, data, &ring, SHARING_MEMBER_CONTEXT),
|
||||
VerificationStatus::Failed
|
||||
);
|
||||
assert_eq!(
|
||||
verify_detached_with_context(&base64, b"other packet", &ring, SHARING_INVITER_CONTEXT),
|
||||
VerificationStatus::Failed
|
||||
);
|
||||
assert_eq!(
|
||||
verify_detached_with_context(
|
||||
&base64,
|
||||
data,
|
||||
&VerificationKeyRing::empty(),
|
||||
SHARING_INVITER_CONTEXT
|
||||
),
|
||||
VerificationStatus::NoVerifier
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_signature_without_a_context_is_not_a_context_signature() {
|
||||
let signer = generate_node_key().expect("generate signer key");
|
||||
let data = b"share passphrase key packet";
|
||||
let armored = signer.key.sign_detached(data).expect("sign");
|
||||
let ring = VerificationKeyRing::from_private(&signer.key);
|
||||
|
||||
assert_eq!(
|
||||
verify_detached(&armored, data, &ring),
|
||||
VerificationStatus::Ok
|
||||
);
|
||||
assert_eq!(
|
||||
verify_detached_with_context(
|
||||
&armored,
|
||||
data,
|
||||
&ring,
|
||||
crate::crypto::SHARING_INVITER_CONTEXT
|
||||
),
|
||||
VerificationStatus::Failed
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inline_signed_message_round_trips_with_status() {
|
||||
let signer = generate_node_key().expect("generate signer key");
|
||||
|
||||
Reference in new issue
Block a user