bumped deps + fixed deprecations + session for live tests

This commit is contained in:
nvrl committed 2026-06-28 11:39:57 +02:00
1 parent b9742e122a
commit 70e573ebe1
10 files changed
+761 -312

No files matched your search

+3
View File
@@ -26,3 +26,6 @@ todo.md
sdk/ sdk/
.env .env
# Cached live-test session (resume instead of re-running SRP+2FA)
.proton_session.json
Generated
+584 -247
View File
File diff suppressed because it is too large. Load diff
+9 -8
View File
@@ -15,7 +15,7 @@ categories = ["api-bindings", "cryptography", "filesystem"]
[workspace.dependencies] [workspace.dependencies]
tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "fs", "io-util", "time"] } tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "fs", "io-util", "time"] }
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls", "stream", "multipart"] } reqwest = { version = "0.13", default-features = false, features = ["json", "rustls", "webpki-roots", "stream", "multipart"] }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
serde_repr = "0.1" serde_repr = "0.1"
@@ -25,18 +25,19 @@ futures = "0.3"
bytes = "1" bytes = "1"
base64 = "0.22" base64 = "0.22"
hex = "0.4" hex = "0.4"
hmac = "0.12" hmac = "0.13"
sha1 = "0.10" sha1 = "0.11"
sha2 = "0.10" sha2 = "0.11"
aes-gcm = "0.10" aes-gcm = "0.10"
hkdf = "0.12" hkdf = "0.13"
bcrypt = "0.17" bcrypt = "0.19"
pgp = "0.16" pgp = "0.20"
# Match the chrono line rPGP pulls in; used to stamp detached-signature subpackets. # Match the chrono line rPGP pulls in; used to stamp detached-signature subpackets.
chrono = "0.4" chrono = "0.4"
tracing = "0.1" tracing = "0.1"
num-bigint = "0.4" num-bigint = "0.4"
num-traits = "0.2" num-traits = "0.2"
getrandom = "0.2" getrandom = "0.2"
# rPGP re-exports rand_core 0.6 traits; match its 0.8 line for keygen/encryption RNG. # rPGP (pgp 0.20) still depends on rand 0.8 and passes its CryptoRng/Rng traits
# across its public API, so the RNG we hand to keygen/encryption must be rand 0.8.
rand = "0.8" rand = "0.8"
@@ -11,7 +11,7 @@
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
use hmac::{Hmac, Mac}; use hmac::{Hmac, KeyInit, Mac};
use proton_drive_sdk::ProtonDriveClient; use proton_drive_sdk::ProtonDriveClient;
use proton_sdk::config::ProtonClientConfiguration; use proton_sdk::config::ProtonClientConfiguration;
use proton_sdk::session::ProtonApiSession; use proton_sdk::session::ProtonApiSession;
+1 -1
View File
@@ -22,7 +22,7 @@ use proton_sdk::ids::{AddressId, DriveEventId, LinkId, NodeUid, ShareId, VolumeI
use proton_sdk::session::ProtonApiSession; use proton_sdk::session::ProtonApiSession;
use proton_sdk::telemetry::{NoopTelemetry, Telemetry, TelemetryExt}; use proton_sdk::telemetry::{NoopTelemetry, Telemetry, TelemetryExt};
use hmac::{Hmac, Mac}; use hmac::{Hmac, KeyInit, Mac};
use sha1::Sha1; use sha1::Sha1;
use crate::cache::DriveEntityCache; use crate::cache::DriveEntityCache;
+105 -4
View File
@@ -13,10 +13,11 @@
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
use hmac::{Hmac, Mac}; use hmac::{Hmac, KeyInit, Mac};
use proton_drive_sdk::ProtonDriveClient; use proton_drive_sdk::ProtonDriveClient;
use proton_sdk::config::ProtonClientConfiguration; use proton_sdk::config::ProtonClientConfiguration;
use proton_sdk::session::ProtonApiSession; use proton_sdk::session::{PasswordMode, ProtonApiSession, ResumeParameters};
use serde::{Deserialize, Serialize};
use sha1::Sha1; use sha1::Sha1;
// Must follow Proton's required shape (sdk/README.md "Operational requirements"). // Must follow Proton's required shape (sdk/README.md "Operational requirements").
@@ -51,6 +52,12 @@ pub async fn live_client() -> Option<&'static LiveClient> {
} }
/// Perform the one-time SRP + 2FA login and build the client. /// Perform the one-time SRP + 2FA login and build the client.
///
/// To avoid burning a TOTP window (and tripping Proton anti-abuse) on every run,
/// a successful login is persisted to a gitignored cache file. On the next run we
/// [`ProtonApiSession::resume`] from that file instead of doing SRP+2FA again; the
/// stored refresh token auto-refreshes via the http client's 401 path, so no TOTP
/// is needed. A missing/dead cache falls back to a fresh login.
async fn build_live_client() -> Option<LiveClient> { async fn build_live_client() -> Option<LiveClient> {
let (username, password) = match read_dotenv() { let (username, password) = match read_dotenv() {
Ok(creds) => creds, Ok(creds) => creds,
@@ -59,6 +66,33 @@ async fn build_live_client() -> Option<LiveClient> {
return None; return None;
} }
}; };
let config = ProtonClientConfiguration::new(APP_VERSION).with_user_agent(USER_AGENT);
// Fast path: resume a cached session (no TOTP). Validate with one cheap
// authenticated call so a dead/revoked session falls through to a fresh login.
if let Some(stored) = load_cached_session() {
let session = ProtonApiSession::resume(config.clone(), stored.into_params())
.expect("resume cached session");
match session
.http()
.get::<serde_json::Value>("core/v4/users")
.await
{
Ok(_) => {
eprintln!("[auth] resumed cached session (no TOTP)");
// Refresh may have rotated the tokens; re-persist the current set.
save_session(&session).await;
let client = ProtonDriveClient::new(&session, password.into_bytes());
return Some(LiveClient {
client,
_session: session,
});
}
Err(e) => eprintln!("[auth] cached session invalid ({e}); logging in fresh"),
}
}
let totp_secret = match read_totp_secret() { let totp_secret = match read_totp_secret() {
Some(s) => s, Some(s) => s,
None => { None => {
@@ -67,8 +101,6 @@ async fn build_live_client() -> Option<LiveClient> {
} }
}; };
let config = ProtonClientConfiguration::new(APP_VERSION).with_user_agent(USER_AGENT);
let mut session = ProtonApiSession::begin(config, &username, password.as_bytes()) let mut session = ProtonApiSession::begin(config, &username, password.as_bytes())
.await .await
.expect("SRP login failed"); .expect("SRP login failed");
@@ -81,6 +113,7 @@ async fn build_live_client() -> Option<LiveClient> {
.expect("2FA submission failed"); .expect("2FA submission failed");
} }
save_session(&session).await;
let client = ProtonDriveClient::new(&session, password.into_bytes()); let client = ProtonDriveClient::new(&session, password.into_bytes());
Some(LiveClient { Some(LiveClient {
client, client,
@@ -88,6 +121,74 @@ async fn build_live_client() -> Option<LiveClient> {
}) })
} }
/// Persisted session credentials. Mirrors [`ResumeParameters`], serialized to a
/// gitignored cache file so reruns can skip SRP+2FA.
#[derive(Serialize, Deserialize)]
struct StoredSession {
session_id: String,
username: String,
user_id: String,
access_token: String,
refresh_token: String,
scopes: Vec<String>,
/// `1` = single, `2` = dual (matches Proton's wire value).
password_mode: u8,
}
impl StoredSession {
fn into_params(self) -> ResumeParameters {
ResumeParameters {
session_id: self.session_id.into(),
username: self.username,
user_id: self.user_id.into(),
access_token: self.access_token,
refresh_token: self.refresh_token,
scopes: self.scopes,
is_waiting_for_second_factor_code: false,
password_mode: match self.password_mode {
1 => PasswordMode::Single,
_ => PasswordMode::Dual,
},
}
}
}
/// Repo-root path of the gitignored session cache.
fn session_cache_path() -> String {
concat!(env!("CARGO_MANIFEST_DIR"), "/../../.proton_session.json").to_owned()
}
/// Load the cached session, or `None` if absent/unparseable.
fn load_cached_session() -> Option<StoredSession> {
let text = std::fs::read_to_string(session_cache_path()).ok()?;
serde_json::from_str(&text).ok()
}
/// Persist the session's current tokens to the cache file (best-effort).
async fn save_session(session: &ProtonApiSession) {
let tokens = session.current_tokens().await;
let stored = StoredSession {
session_id: session.session_id().as_str().to_owned(),
username: session.username().to_owned(),
user_id: session.user_id().as_str().to_owned(),
access_token: tokens.access_token,
refresh_token: tokens.refresh_token,
scopes: session.scopes().to_vec(),
password_mode: match session.password_mode() {
PasswordMode::Single => 1,
PasswordMode::Dual => 2,
},
};
match serde_json::to_string_pretty(&stored) {
Ok(json) => {
if let Err(e) = std::fs::write(session_cache_path(), json) {
eprintln!("[auth] could not write session cache: {e}");
}
}
Err(e) => eprintln!("[auth] could not serialize session: {e}"),
}
}
/// Minimal `.env` reader: `key=value` lines, trims whitespace, ignores `#`. /// Minimal `.env` reader: `key=value` lines, trims whitespace, ignores `#`.
fn read_dotenv() -> Result<(String, String), Box<dyn std::error::Error>> { fn read_dotenv() -> Result<(String, String), Box<dyn std::error::Error>> {
let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../../.env"); let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../../.env");
+29 -23
View File
@@ -9,7 +9,7 @@
use std::io::Cursor; use std::io::Cursor;
use pgp::armor::Dearmor; use pgp::armor::Dearmor;
use pgp::composed::{MessageBuilder, PlainSessionKey}; use pgp::composed::{MessageBuilder, PlainSessionKey, RawSessionKey};
use pgp::crypto::aead::{AeadAlgorithm, ChunkSize}; use pgp::crypto::aead::{AeadAlgorithm, ChunkSize};
use pgp::crypto::hash::HashAlgorithm; use pgp::crypto::hash::HashAlgorithm;
use pgp::crypto::sym::SymmetricKeyAlgorithm; use pgp::crypto::sym::SymmetricKeyAlgorithm;
@@ -17,7 +17,7 @@ use pgp::packet::{
LiteralData, Packet, PacketParser, PacketTrait, PublicKeyEncryptedSessionKey, LiteralData, Packet, PacketParser, PacketTrait, PublicKeyEncryptedSessionKey,
SymEncryptedProtectedData, SymEncryptedProtectedData,
}; };
use pgp::types::{EskType, PkeskVersion, PublicKeyTrait}; use pgp::types::{DecryptionKey, EncryptionKey, EskType, PkeskVersion, Seipdv1ReadMode};
use super::encrypt::{recipient_encryption_key, RecipientOp}; use super::encrypt::{recipient_encryption_key, RecipientOp};
use super::errors::CryptoError; use super::errors::CryptoError;
@@ -49,7 +49,7 @@ impl ContentKey {
pub fn generate() -> Self { pub fn generate() -> Self {
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
let sym_alg = SymmetricKeyAlgorithm::AES256; let sym_alg = SymmetricKeyAlgorithm::AES256;
let key = sym_alg.new_session_key(&mut rng).to_vec(); let key = sym_alg.new_session_key(&mut rng);
ContentKey { ContentKey {
session_key: PlainSessionKey::V3_4 { sym_alg, key }, session_key: PlainSessionKey::V3_4 { sym_alg, key },
} }
@@ -62,9 +62,7 @@ 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 let key = SymmetricKeyAlgorithm::AES256.new_session_key(&mut rng);
.new_session_key(&mut rng)
.to_vec();
ContentKey { ContentKey {
session_key: PlainSessionKey::V6 { key }, session_key: PlainSessionKey::V6 { key },
} }
@@ -191,8 +189,8 @@ impl ContentKey {
/// official clients, which hardcode it for v6 session keys. /// official clients, which hardcode it for v6 session keys.
fn session_parts(&self) -> Result<(SymmetricKeyAlgorithm, &[u8]), CryptoError> { fn session_parts(&self) -> Result<(SymmetricKeyAlgorithm, &[u8]), CryptoError> {
match &self.session_key { match &self.session_key {
PlainSessionKey::V3_4 { sym_alg, key } => Ok((*sym_alg, key)), PlainSessionKey::V3_4 { sym_alg, key } => Ok((*sym_alg, key.as_ref())),
PlainSessionKey::V6 { key } => Ok((SymmetricKeyAlgorithm::AES256, key)), PlainSessionKey::V6 { key } => Ok((SymmetricKeyAlgorithm::AES256, key.as_ref())),
_ => Err(CryptoError::Encrypt( _ => Err(CryptoError::Encrypt(
"content key is not a V3/V4 or V6 session key".into(), "content key is not a V3/V4 or V6 session key".into(),
)), )),
@@ -284,7 +282,7 @@ impl ContentKey {
}; };
seipd seipd
.decrypt(key, Some(sym_alg)) .decrypt(key, Some(sym_alg), Seipdv1ReadMode::default())
.map_err(|e| CryptoError::Decrypt(format!("block: {e}"))) .map_err(|e| CryptoError::Decrypt(format!("block: {e}")))
} }
} }
@@ -322,13 +320,16 @@ impl PrivateKey {
let pw = self.password(); let pw = self.password();
let key = self.key(); let key = self.key();
// pgp 0.20: `DecryptionKey::decrypt` is implemented on the packet secret
// (sub)keys (`SecretKey` / `SecretSubkey`), not on `SignedSecretKey`, so
// reach through `primary_key` and each subkey's `.key`.
let mut last_err: CryptoError; let mut last_err: CryptoError;
match key.decrypt_session_key(&pw, values, esk_type) { match key.primary_key.decrypt(&pw, values, esk_type) {
Ok(Ok(session_key)) => return Ok(ContentKey { session_key }), Ok(Ok(session_key)) => return Ok(ContentKey { session_key }),
Ok(Err(e)) | Err(e) => last_err = CryptoError::Decrypt(format!("content key: {e}")), Ok(Err(e)) | Err(e) => last_err = CryptoError::Decrypt(format!("content key: {e}")),
} }
for subkey in &key.secret_subkeys { for subkey in &key.secret_subkeys {
match subkey.decrypt_session_key(&pw, values, esk_type) { match subkey.key.decrypt(&pw, values, esk_type) {
Ok(Ok(session_key)) => return Ok(ContentKey { session_key }), Ok(Ok(session_key)) => return Ok(ContentKey { session_key }),
Ok(Err(e)) | Err(e) => { Ok(Err(e)) | Err(e) => {
last_err = CryptoError::Decrypt(format!("content key (subkey): {e}")) last_err = CryptoError::Decrypt(format!("content key (subkey): {e}"))
@@ -428,9 +429,15 @@ struct ContentKeyPacketOp<'a> {
impl RecipientOp for ContentKeyPacketOp<'_> { impl RecipientOp for ContentKeyPacketOp<'_> {
type Out = PublicKeyEncryptedSessionKey; type Out = PublicKeyEncryptedSessionKey;
fn run(self, pubkey: &impl PublicKeyTrait) -> pgp::errors::Result<Self::Out> { fn run(self, pubkey: &impl EncryptionKey) -> pgp::errors::Result<Self::Out> {
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
PublicKeyEncryptedSessionKey::from_session_key_v3(&mut rng, self.key, self.sym_alg, pubkey) let session_key = RawSessionKey::from(self.key);
PublicKeyEncryptedSessionKey::from_session_key_v3(
&mut rng,
&session_key,
self.sym_alg,
pubkey,
)
} }
} }
@@ -443,16 +450,19 @@ struct ContentKeyPacketV6Op<'a> {
impl RecipientOp for ContentKeyPacketV6Op<'_> { impl RecipientOp for ContentKeyPacketV6Op<'_> {
type Out = PublicKeyEncryptedSessionKey; type Out = PublicKeyEncryptedSessionKey;
fn run(self, pubkey: &impl PublicKeyTrait) -> pgp::errors::Result<Self::Out> { fn run(self, pubkey: &impl EncryptionKey) -> pgp::errors::Result<Self::Out> {
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
PublicKeyEncryptedSessionKey::from_session_key_v6(&mut rng, self.key, pubkey) let session_key = RawSessionKey::from(self.key);
PublicKeyEncryptedSessionKey::from_session_key_v6(&mut rng, &session_key, pubkey)
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use pgp::composed::{KeyType, MessageBuilder, SecretKeyParamsBuilder, SignedSecretKey}; use pgp::composed::{
EncryptionCaps, 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
@@ -463,16 +473,12 @@ mod tests {
.key_type(KeyType::Rsa(2048)) .key_type(KeyType::Rsa(2048))
.can_sign(true) .can_sign(true)
.can_certify(true) .can_certify(true)
.can_encrypt(true) .can_encrypt(EncryptionCaps::All)
.primary_user_id("node <node@proton.test>".into()) .primary_user_id("node <node@proton.test>".into())
.passphrase(Some(passphrase.to_owned())) .passphrase(Some(passphrase.to_owned()))
.build() .build()
.expect("key params"); .expect("key params");
params params.generate(&mut rng).expect("generate")
.generate(&mut rng)
.expect("generate")
.sign(&mut rng, &passphrase.into())
.expect("sign")
} }
#[test] #[test]
@@ -576,7 +582,7 @@ mod tests {
other => panic!("expected SEIPD, got {:?}", other.tag()), other => panic!("expected SEIPD, got {:?}", other.tag()),
}; };
let inner = seipd let inner = seipd
.decrypt(key, Some(sym_alg)) .decrypt(key, Some(sym_alg), Seipdv1ReadMode::default())
.expect("decrypt thumbnail"); .expect("decrypt thumbnail");
let mut inner_parser = PacketParser::new(Cursor::new(&inner)); let mut inner_parser = PacketParser::new(Cursor::new(&inner));
+20 -19
View File
@@ -9,15 +9,14 @@
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, DetachedSignature, EncryptionCaps, KeyType, MessageBuilder,
StandaloneSignature, SubkeyParamsBuilder, SecretKeyParamsBuilder, SignedSecretKey, SubkeyParamsBuilder,
}; };
use pgp::crypto::hash::HashAlgorithm; 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, EncryptionKey, KeyDetails, KeyVersion};
use rand::RngCore; use rand::RngCore;
use super::errors::CryptoError; use super::errors::CryptoError;
@@ -34,7 +33,7 @@ pub(crate) trait RecipientOp {
/// The value produced by the operation. /// The value produced by the operation.
type Out; type Out;
/// Run the operation against the selected public key. /// Run the operation against the selected public key.
fn run(self, pubkey: &impl PublicKeyTrait) -> pgp::errors::Result<Self::Out>; fn run(self, pubkey: &impl EncryptionKey) -> pgp::errors::Result<Self::Out>;
} }
/// Run `op` against the encryption-capable public key of `key`. /// Run `op` against the encryption-capable public key of `key`.
@@ -47,14 +46,14 @@ pub(crate) fn recipient_encryption_key<Op: RecipientOp>(
op: Op, op: Op,
) -> Result<Op::Out, CryptoError> { ) -> Result<Op::Out, CryptoError> {
let primary = key.primary_key.public_key(); let primary = key.primary_key.public_key();
if primary.is_encryption_key() { if primary.algorithm().can_encrypt() {
return op return op
.run(primary) .run(primary)
.map_err(|e| CryptoError::Encrypt(format!("encrypt to primary key: {e}"))); .map_err(|e| CryptoError::Encrypt(format!("encrypt to primary key: {e}")));
} }
for sub in &key.secret_subkeys { for sub in &key.secret_subkeys {
let pubsub = sub.public_key(); let pubsub = sub.public_key();
if pubsub.is_encryption_key() { if pubsub.algorithm().can_encrypt() {
return op return op
.run(&pubsub) .run(&pubsub)
.map_err(|e| CryptoError::Encrypt(format!("encrypt to subkey: {e}"))); .map_err(|e| CryptoError::Encrypt(format!("encrypt to subkey: {e}")));
@@ -108,7 +107,7 @@ fn generate_node_key_versioned(version: KeyVersion) -> Result<GeneratedNodeKey,
let subkey = SubkeyParamsBuilder::default() let subkey = SubkeyParamsBuilder::default()
.version(version) .version(version)
.key_type(KeyType::X25519) .key_type(KeyType::X25519)
.can_encrypt(true) .can_encrypt(EncryptionCaps::All)
.passphrase(Some(pw_string.clone())) .passphrase(Some(pw_string.clone()))
.build() .build()
.map_err(|e| CryptoError::Encrypt(format!("node subkey params: {e}")))?; .map_err(|e| CryptoError::Encrypt(format!("node subkey params: {e}")))?;
@@ -124,12 +123,11 @@ fn generate_node_key_versioned(version: KeyVersion) -> Result<GeneratedNodeKey,
.build() .build()
.map_err(|e| CryptoError::Encrypt(format!("node key params: {e}")))?; .map_err(|e| CryptoError::Encrypt(format!("node key params: {e}")))?;
let secret = params // In pgp 0.20 `generate` already returns a `SignedSecretKey`, locked with the
// passphrase set on the params (no separate sign/lock step as in 0.16).
let signed = params
.generate(&mut rng) .generate(&mut rng)
.map_err(|e| CryptoError::Encrypt(format!("generate node key: {e}")))?; .map_err(|e| CryptoError::Encrypt(format!("generate node key: {e}")))?;
let signed = secret
.sign(&mut rng, &Password::from(passphrase.as_slice()))
.map_err(|e| CryptoError::Encrypt(format!("sign node key: {e}")))?;
let locked_armored = signed let locked_armored = signed
.to_armored_string(None.into()) .to_armored_string(None.into())
.map_err(|e| CryptoError::Encrypt(format!("armor node key: {e}")))?; .map_err(|e| CryptoError::Encrypt(format!("armor node key: {e}")))?;
@@ -275,20 +273,23 @@ pub(crate) fn sign_detached(signer: &PrivateKey, data: &[u8]) -> Result<String,
Subpacket::regular(SubpacketData::IssuerFingerprint(key.fingerprint())) Subpacket::regular(SubpacketData::IssuerFingerprint(key.fingerprint()))
.map_err(|e| CryptoError::Encrypt(format!("issuer fingerprint subpacket: {e}")))?, .map_err(|e| CryptoError::Encrypt(format!("issuer fingerprint subpacket: {e}")))?,
Subpacket::regular(SubpacketData::SignatureCreationTime( Subpacket::regular(SubpacketData::SignatureCreationTime(
chrono::Utc::now().trunc_subsecs(0), pgp::types::Timestamp::now(),
)) ))
.map_err(|e| CryptoError::Encrypt(format!("creation-time subpacket: {e}")))?, .map_err(|e| CryptoError::Encrypt(format!("creation-time subpacket: {e}")))?,
]; ];
if key.version() <= KeyVersion::V4 { if key.version() <= KeyVersion::V4 {
config.unhashed_subpackets = vec![Subpacket::regular(SubpacketData::Issuer(key.key_id())) config.unhashed_subpackets =
.map_err(|e| CryptoError::Encrypt(format!("issuer subpacket: {e}")))?]; vec![
Subpacket::regular(SubpacketData::IssuerKeyId(key.legacy_key_id()))
.map_err(|e| CryptoError::Encrypt(format!("issuer subpacket: {e}")))?,
];
} }
let signature = config let signature = config
.sign(key, &pw, data) .sign(key, &pw, data)
.map_err(|e| CryptoError::Encrypt(format!("detached sign: {e}")))?; .map_err(|e| CryptoError::Encrypt(format!("detached sign: {e}")))?;
StandaloneSignature::new(signature) DetachedSignature::new(signature)
.to_armored_string(ArmorOptions::default()) .to_armored_string(ArmorOptions::default())
.map_err(|e| CryptoError::Encrypt(format!("armor signature: {e}"))) .map_err(|e| CryptoError::Encrypt(format!("armor signature: {e}")))
} }
@@ -304,7 +305,7 @@ struct EncryptSignOp<'a> {
impl RecipientOp for EncryptSignOp<'_> { impl RecipientOp for EncryptSignOp<'_> {
type Out = String; type Out = String;
fn run(self, pubkey: &impl PublicKeyTrait) -> pgp::errors::Result<String> { fn run(self, pubkey: &impl EncryptionKey) -> pgp::errors::Result<String> {
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
let mut builder = MessageBuilder::from_bytes(Bytes::new(), self.data) let mut builder = MessageBuilder::from_bytes(Bytes::new(), self.data)
.seipd_v1(&mut rng, SymmetricKeyAlgorithm::AES256); .seipd_v1(&mut rng, SymmetricKeyAlgorithm::AES256);
@@ -370,13 +371,13 @@ mod tests {
// rejects with "Invalid manifest signature". rPGP self-verify above is not // rejects with "Invalid manifest signature". rPGP self-verify above is not
// enough to catch that — assert the subpackets explicitly. // enough to catch that — assert the subpackets explicitly.
use pgp::composed::Deserializable as _; use pgp::composed::Deserializable as _;
let (parsed, _) = StandaloneSignature::from_string(&sig).expect("parse detached sig"); let (parsed, _) = DetachedSignature::from_string(&sig).expect("parse detached sig");
assert!( assert!(
parsed.signature.created().is_some(), parsed.signature.created().is_some(),
"detached signature missing creation time" "detached signature missing creation time"
); );
assert!( assert!(
!parsed.signature.issuer().is_empty(), !parsed.signature.issuer_key_id().is_empty(),
"detached signature missing issuer key id" "detached signature missing issuer key id"
); );
} }
+4 -4
View File
@@ -4,7 +4,7 @@
//! distinct pre-unlock step, so a [`PrivateKey`] simply pairs a parsed //! distinct pre-unlock step, so a [`PrivateKey`] simply pairs a parsed
//! [`SignedSecretKey`] with the passphrase that unlocks it. //! [`SignedSecretKey`] with the passphrase that unlocks it.
use pgp::composed::{Deserializable, SignedSecretKey, StandaloneSignature}; use pgp::composed::{Deserializable, SignedSecretKey, DetachedSignature};
use pgp::types::Password; use pgp::types::Password;
use super::errors::CryptoError; use super::errors::CryptoError;
@@ -64,7 +64,7 @@ impl PrivateKey {
/// The public half of this key, for use as an anonymous-fallback verifier. /// The public half of this key, for use as an anonymous-fallback verifier.
pub(crate) fn signed_public_key(&self) -> pgp::composed::SignedPublicKey { pub(crate) fn signed_public_key(&self) -> pgp::composed::SignedPublicKey {
self.key.signed_public_key() self.key.to_public_key()
} }
/// Verify an armored detached signature over `data` against this key's /// Verify an armored detached signature over `data` against this key's
@@ -74,9 +74,9 @@ impl PrivateKey {
armored_sig: &str, armored_sig: &str,
data: &[u8], data: &[u8],
) -> Result<(), CryptoError> { ) -> Result<(), CryptoError> {
let (sig, _headers) = StandaloneSignature::from_string(armored_sig) let (sig, _headers) = DetachedSignature::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.to_public_key(), data)
.map_err(|e| CryptoError::Verification(e.to_string())) .map_err(|e| CryptoError::Verification(e.to_string()))
} }
+5 -5
View File
@@ -9,10 +9,10 @@
//! throwing. //! throwing.
use pgp::composed::{ use pgp::composed::{
Deserializable, Message, SignedPublicKey, SignedSecretKey, StandaloneSignature, Deserializable, Message, SignedPublicKey, SignedSecretKey, DetachedSignature,
VerificationResult, VerificationResult,
}; };
use pgp::types::{Password, PublicKeyTrait}; use pgp::types::{Password, VerifyingKey};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::errors::CryptoError; use super::errors::CryptoError;
@@ -111,8 +111,8 @@ impl VerificationKeyRing {
self.keys.is_empty() self.keys.is_empty()
} }
fn dyn_refs(&self) -> Vec<&dyn PublicKeyTrait> { fn dyn_refs(&self) -> Vec<&dyn VerifyingKey> {
self.keys.iter().map(|k| k as &dyn PublicKeyTrait).collect() self.keys.iter().map(|k| k as &dyn VerifyingKey).collect()
} }
} }
@@ -122,7 +122,7 @@ pub fn verify_detached(
data: &[u8], data: &[u8],
ring: &VerificationKeyRing, ring: &VerificationKeyRing,
) -> VerificationStatus { ) -> VerificationStatus {
let signature = match StandaloneSignature::from_string(armored_sig) { let signature = match DetachedSignature::from_string(armored_sig) {
Ok((sig, _headers)) => sig, Ok((sig, _headers)) => sig,
Err(_) => return VerificationStatus::Failed, Err(_) => return VerificationStatus::Failed,
}; };