mirror of
https://github.com/narrrl/proton-sdk-rs.git
synced 2026-11-03 16:41:26 +01:00
bumped deps + fixed deprecations + session for live tests
This commit is contained in:
1 parent
b9742e122a
commit
70e573ebe1
10 files changed
+761
-312
No files matched your search
@@ -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
File diff suppressed because it is too large.
Load diff
+9
-8
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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,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()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in new issue
Block a user