mirror of
https://github.com/narrrl/proton-sdk-rs.git
synced 2026-11-03 16:41:26 +01:00
feat(account,drive): report which keys open and which key each token is for
AccountClient::key_report walks the user and address keys like the unlock path, but records each step instead of skipping what fails: whether each user key opens (inactive ones included), and the recipients of each address key token. ProtonDriveClient:: my_files_share_recipients names the keys the My Files share passphrase is encrypted to without decrypting anything. Together they trace a "missing key" to the key it was meant for (proton-drive-linux#40). crypto::recipients and crypto::key_ids read OpenPGP identifiers from messages and keys, so a recipient can be matched to a key.
This commit is contained in:
1 parent
155bf23615
commit
bd27f7862b
6 files changed
+326
-4
No files matched your search
@@ -555,6 +555,18 @@ impl ProtonDriveClient {
|
|||||||
&self.account
|
&self.account
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The address the My Files share belongs to, and the keys its passphrase
|
||||||
|
/// is encrypted to (see [`proton_sdk::crypto::recipients`]). Decrypts
|
||||||
|
/// nothing and creates nothing, so it answers even when the share key does
|
||||||
|
/// not open: matched against
|
||||||
|
/// [`AccountClient::key_report`](proton_sdk::account::AccountClient::key_report),
|
||||||
|
/// it names the address key My Files needs.
|
||||||
|
pub async fn my_files_share_recipients(&self) -> Result<(AddressId, Vec<Option<String>>)> {
|
||||||
|
let response: MyFilesShareResponse = self.http.get("v2/shares/my-files").await?;
|
||||||
|
let recipients = proton_sdk::crypto::recipients(&response.share.passphrase)?;
|
||||||
|
Ok((response.share.address_id, recipients))
|
||||||
|
}
|
||||||
|
|
||||||
/// Total account storage usage (`MaxSpace`/`UsedSpace`), account-wide across
|
/// Total account storage usage (`MaxSpace`/`UsedSpace`), account-wide across
|
||||||
/// all Proton products. Convenience passthrough to
|
/// all Proton products. Convenience passthrough to
|
||||||
/// [`AccountClient::quota`](proton_sdk::account::AccountClient::quota).
|
/// [`AccountClient::quota`](proton_sdk::account::AccountClient::quota).
|
||||||
|
|||||||
@@ -12,8 +12,10 @@
|
|||||||
|
|
||||||
mod dtos;
|
mod dtos;
|
||||||
mod passphrases;
|
mod passphrases;
|
||||||
|
mod report;
|
||||||
|
|
||||||
pub use passphrases::KeyPassphrases;
|
pub use passphrases::KeyPassphrases;
|
||||||
|
pub use report::{AddressKeyReport, AddressReport, KeyReport, UserKeyReport};
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -621,6 +623,65 @@ mod tests {
|
|||||||
assert!(matches!(error, ProtonError::KeysLocked), "{error:?}");
|
assert!(matches!(error, ProtonError::KeysLocked), "{error:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn the_key_report_traces_a_token_to_the_inactive_key_it_was_meant_for() {
|
||||||
|
use crate::crypto::{PrivateKey, generate_node_key};
|
||||||
|
|
||||||
|
let active = generate_node_key().unwrap();
|
||||||
|
let inactive = generate_node_key().unwrap();
|
||||||
|
let (good, stranded) = (generate_node_key().unwrap(), generate_node_key().unwrap());
|
||||||
|
let token_for = |user_key: &crate::crypto::GeneratedNodeKey, passphrase: &[u8]| {
|
||||||
|
PrivateKey::from_armored(&user_key.locked_armored, &user_key.passphrase)
|
||||||
|
.unwrap()
|
||||||
|
.encrypt(passphrase)
|
||||||
|
.unwrap()
|
||||||
|
};
|
||||||
|
// Both answers in one body: each request reads only its own field.
|
||||||
|
let body = serde_json::json!({
|
||||||
|
"Code": 1000,
|
||||||
|
"User": {
|
||||||
|
"ID": "user-id", "MaxSpace": 1, "UsedSpace": 0,
|
||||||
|
"Keys": [
|
||||||
|
{ "ID": "key-1", "PrivateKey": active.locked_armored, "Primary": 1, "Active": 1 },
|
||||||
|
{ "ID": "key-2", "PrivateKey": inactive.locked_armored, "Primary": 0, "Active": 0 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Addresses": [{
|
||||||
|
"ID": "address-1", "Email": "user@proton.me", "Status": 1, "Order": 1,
|
||||||
|
"Keys": [
|
||||||
|
{ "ID": "address-key-1", "PrivateKey": good.locked_armored,
|
||||||
|
"Token": token_for(&active, &good.passphrase), "Signature": "signature",
|
||||||
|
"Primary": 1, "Active": 1 },
|
||||||
|
{ "ID": "address-key-2", "PrivateKey": stranded.locked_armored,
|
||||||
|
"Token": token_for(&inactive, &stranded.passphrase), "Signature": "signature",
|
||||||
|
"Primary": 0, "Active": 1 }
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
let (session, _) = session_serving(body.to_string()).await;
|
||||||
|
let passphrases = KeyPassphrases::from_iter([
|
||||||
|
("key-1", active.passphrase.clone()),
|
||||||
|
("key-2", inactive.passphrase.clone()),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let report = AccountClient::with_key_passphrases(&session, passphrases)
|
||||||
|
.key_report()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(report.user_keys.iter().all(|key| key.unlocks));
|
||||||
|
let keys = &report.addresses[0].keys;
|
||||||
|
assert!(keys[0].unlocks && keys[0].error.is_none());
|
||||||
|
assert!(!keys[1].unlocks);
|
||||||
|
assert!(keys[1].error.as_deref().unwrap().contains("missing key"));
|
||||||
|
let recipients = keys[1].token_recipients.as_ref().unwrap();
|
||||||
|
let meant_for = report
|
||||||
|
.user_key_named(recipients[0].as_deref().unwrap())
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(meant_for.id.as_str(), "key-2");
|
||||||
|
assert!(!meant_for.active);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn an_account_without_an_active_key_is_not_a_password_problem() {
|
async fn an_account_without_an_active_key_is_not_a_password_problem() {
|
||||||
let key = crate::crypto::generate_node_key().unwrap();
|
let key = crate::crypto::generate_node_key().unwrap();
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
//! A report on the account's key chain, for diagnosing keys that do not open.
|
||||||
|
//!
|
||||||
|
//! [`AccountClient::key_report`] walks the same chain as the unlock path in the
|
||||||
|
//! parent module, but records every step instead of skipping what fails: each
|
||||||
|
//! user key and whether the client's passphrases open it, and for each address
|
||||||
|
//! key the keys its token is encrypted to. A token that fails with "missing
|
||||||
|
//! key" can then be traced to the user key it was meant for.
|
||||||
|
|
||||||
|
use crate::crypto::{self, PrivateKey};
|
||||||
|
use crate::error::Result;
|
||||||
|
use crate::ids::{AddressId, AddressKeyId, UserKeyId};
|
||||||
|
|
||||||
|
use super::AccountClient;
|
||||||
|
use super::dtos::{AddressListResponse, UserResponse};
|
||||||
|
|
||||||
|
/// The account's keys as this client sees them. Holds identifiers and flags
|
||||||
|
/// only, never key material, so it is safe to show and to share.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct KeyReport {
|
||||||
|
pub user_keys: Vec<UserKeyReport>,
|
||||||
|
pub addresses: Vec<AddressReport>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One user (account) key.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct UserKeyReport {
|
||||||
|
pub id: UserKeyId,
|
||||||
|
pub primary: bool,
|
||||||
|
/// `Active` from the server. The unlock path uses active keys only.
|
||||||
|
pub active: bool,
|
||||||
|
/// Whether the client's passphrase for this key opens it. Tried for
|
||||||
|
/// inactive keys too, which the unlock path never does.
|
||||||
|
pub unlocks: bool,
|
||||||
|
/// Why the key does not open, when it does not.
|
||||||
|
pub error: Option<String>,
|
||||||
|
/// The key's OpenPGP identifiers, see [`crypto::key_ids`].
|
||||||
|
pub key_ids: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One address and its keys.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct AddressReport {
|
||||||
|
pub id: AddressId,
|
||||||
|
pub email: String,
|
||||||
|
pub keys: Vec<AddressKeyReport>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One address key.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct AddressKeyReport {
|
||||||
|
pub id: AddressKeyId,
|
||||||
|
pub primary: bool,
|
||||||
|
pub active: bool,
|
||||||
|
/// The keys the token is encrypted to, see [`crypto::recipients`]. `None`
|
||||||
|
/// for a key without a token, whose passphrase comes from the password.
|
||||||
|
pub token_recipients: Option<Vec<Option<String>>>,
|
||||||
|
/// Whether the key opens the way the unlock path opens it: through its
|
||||||
|
/// token with the active user keys that open, or through the password.
|
||||||
|
pub unlocks: bool,
|
||||||
|
/// Why the key does not open, when it does not.
|
||||||
|
pub error: Option<String>,
|
||||||
|
/// The key's OpenPGP identifiers, see [`crypto::key_ids`].
|
||||||
|
pub key_ids: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeyReport {
|
||||||
|
/// The user key a message recipient (from [`crypto::recipients`]) names.
|
||||||
|
pub fn user_key_named(&self, recipient: &str) -> Option<&UserKeyReport> {
|
||||||
|
self.user_keys
|
||||||
|
.iter()
|
||||||
|
.find(|key| key.key_ids.iter().any(|id| id == recipient))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The address key a message recipient names, with its address.
|
||||||
|
pub fn address_key_named(
|
||||||
|
&self,
|
||||||
|
recipient: &str,
|
||||||
|
) -> Option<(&AddressReport, &AddressKeyReport)> {
|
||||||
|
self.addresses.iter().find_map(|address| {
|
||||||
|
address
|
||||||
|
.keys
|
||||||
|
.iter()
|
||||||
|
.find(|key| key.key_ids.iter().any(|id| id == recipient))
|
||||||
|
.map(|key| (address, key))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AccountClient {
|
||||||
|
/// Report on every user and address key, see [`KeyReport`].
|
||||||
|
///
|
||||||
|
/// Always asks the server (`core/v4/users`, `core/v4/addresses`) and caches
|
||||||
|
/// nothing, so it reflects the account as it is now. Fails only when the
|
||||||
|
/// server cannot be reached or the passphrases cannot be derived; a key
|
||||||
|
/// that does not open is part of the report, not an error.
|
||||||
|
pub async fn key_report(&self) -> Result<KeyReport> {
|
||||||
|
let passphrases = self.key_passphrases().await?;
|
||||||
|
let user: UserResponse = self.inner.http.get("core/v4/users").await?;
|
||||||
|
|
||||||
|
let mut user_keys = Vec::with_capacity(user.user.keys.len());
|
||||||
|
let mut active_unlocked = Vec::new();
|
||||||
|
for dto in &user.user.keys {
|
||||||
|
let unlocked = match passphrases.get(dto.id.as_str()) {
|
||||||
|
Some(passphrase) => PrivateKey::from_armored(&dto.private_key, passphrase)
|
||||||
|
.map_err(|e| e.to_string()),
|
||||||
|
None => Err("no passphrase for this key".to_string()),
|
||||||
|
};
|
||||||
|
let (unlocks, error) = match unlocked {
|
||||||
|
Ok(key) => {
|
||||||
|
if dto.is_active() {
|
||||||
|
active_unlocked.push(key);
|
||||||
|
}
|
||||||
|
(true, None)
|
||||||
|
}
|
||||||
|
Err(e) => (false, Some(e)),
|
||||||
|
};
|
||||||
|
user_keys.push(UserKeyReport {
|
||||||
|
id: dto.id.clone(),
|
||||||
|
primary: dto.primary != 0,
|
||||||
|
active: dto.is_active(),
|
||||||
|
unlocks,
|
||||||
|
error,
|
||||||
|
key_ids: crypto::key_ids(&dto.private_key).unwrap_or_default(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let response: AddressListResponse = self.inner.http.get("core/v4/addresses").await?;
|
||||||
|
let mut addresses = Vec::with_capacity(response.addresses.len());
|
||||||
|
for dto in &response.addresses {
|
||||||
|
let mut keys = Vec::with_capacity(dto.keys.len());
|
||||||
|
for key_dto in &dto.keys {
|
||||||
|
let passphrase = match (&key_dto.token, &key_dto.signature) {
|
||||||
|
(Some(token), Some(_signature)) => {
|
||||||
|
crypto::decrypt_armored_with_keys(token, &active_unlocked)
|
||||||
|
.map_err(|e| format!("token: {e}"))
|
||||||
|
}
|
||||||
|
_ => passphrases
|
||||||
|
.get(key_dto.id.as_str())
|
||||||
|
.map(<[u8]>::to_vec)
|
||||||
|
.ok_or_else(|| "no passphrase for this key".to_string()),
|
||||||
|
};
|
||||||
|
let unlocked = passphrase.and_then(|passphrase| {
|
||||||
|
PrivateKey::from_armored(&key_dto.private_key, &passphrase)
|
||||||
|
.map(drop)
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
});
|
||||||
|
keys.push(AddressKeyReport {
|
||||||
|
id: key_dto.id.clone(),
|
||||||
|
primary: key_dto.is_primary(),
|
||||||
|
active: key_dto.is_active(),
|
||||||
|
token_recipients: key_dto
|
||||||
|
.token
|
||||||
|
.as_deref()
|
||||||
|
.map(|token| crypto::recipients(token).unwrap_or_default()),
|
||||||
|
unlocks: unlocked.is_ok(),
|
||||||
|
error: unlocked.err(),
|
||||||
|
key_ids: crypto::key_ids(&key_dto.private_key).unwrap_or_default(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
addresses.push(AddressReport {
|
||||||
|
id: dto.id.clone(),
|
||||||
|
email: dto.email.clone(),
|
||||||
|
keys,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(KeyReport {
|
||||||
|
user_keys,
|
||||||
|
addresses,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,8 +23,8 @@ pub use encrypt::{
|
|||||||
encrypt_invitation, generate_node_hash_key, generate_node_key, generate_node_key_aead,
|
encrypt_invitation, generate_node_hash_key, generate_node_key, generate_node_key_aead,
|
||||||
};
|
};
|
||||||
pub use errors::CryptoError;
|
pub use errors::CryptoError;
|
||||||
pub use keys::{PrivateKey, decrypt_armored_with_keys};
|
pub use keys::{PrivateKey, decrypt_armored_with_keys, key_ids};
|
||||||
pub use messages::decrypt_armored_with_password;
|
pub use messages::{decrypt_armored_with_password, recipients};
|
||||||
pub use srp::{DEFAULT_BIT_LENGTH, SrpProofs, SrpVerifier, generate_proofs, generate_verifier};
|
pub use srp::{DEFAULT_BIT_LENGTH, SrpProofs, SrpVerifier, generate_proofs, generate_verifier};
|
||||||
pub use verify::{
|
pub use verify::{
|
||||||
PublicKey, VerificationKeyRing, VerificationStatus, verify_detached,
|
PublicKey, VerificationKeyRing, VerificationStatus, verify_detached,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
//! re-run the key's S2K derivation.
|
//! re-run the key's S2K derivation.
|
||||||
|
|
||||||
use pgp::composed::{Deserializable, DetachedSignature, SignedSecretKey};
|
use pgp::composed::{Deserializable, DetachedSignature, SignedSecretKey};
|
||||||
use pgp::types::Password;
|
use pgp::types::{KeyDetails, Password};
|
||||||
|
|
||||||
use super::errors::CryptoError;
|
use super::errors::CryptoError;
|
||||||
use super::messages;
|
use super::messages;
|
||||||
@@ -138,6 +138,23 @@ impl PrivateKey {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Every identifier a message's recipient can name this armored key by, as
|
||||||
|
/// lowercase hex: the key ID and the fingerprint of the primary key and of
|
||||||
|
/// each subkey. Reads only the public parts, so no passphrase is needed.
|
||||||
|
pub fn key_ids(armored: &str) -> Result<Vec<String>, CryptoError> {
|
||||||
|
let (key, _headers) = SignedSecretKey::from_string(armored)
|
||||||
|
.map_err(|e| CryptoError::Parse(format!("secret key: {e}")))?;
|
||||||
|
let mut ids = vec![
|
||||||
|
key.primary_key.legacy_key_id().to_string(),
|
||||||
|
format!("{:x}", key.primary_key.fingerprint()),
|
||||||
|
];
|
||||||
|
for subkey in &key.secret_subkeys {
|
||||||
|
ids.push(subkey.key.legacy_key_id().to_string());
|
||||||
|
ids.push(format!("{:x}", subkey.key.fingerprint()));
|
||||||
|
}
|
||||||
|
Ok(ids)
|
||||||
|
}
|
||||||
|
|
||||||
/// 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(
|
pub fn decrypt_armored_with_keys(
|
||||||
armored: &str,
|
armored: &str,
|
||||||
@@ -158,6 +175,31 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::crypto::encrypt::generate_node_key;
|
use crate::crypto::encrypt::generate_node_key;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_message_names_the_key_it_is_encrypted_to() {
|
||||||
|
let node = generate_node_key().expect("generate node key");
|
||||||
|
let other = generate_node_key().expect("generate node key");
|
||||||
|
let key = PrivateKey::from_armored(&node.locked_armored, &node.passphrase).unwrap();
|
||||||
|
let message = key.encrypt(b"token").unwrap();
|
||||||
|
|
||||||
|
let recipients = super::super::recipients(&message).unwrap();
|
||||||
|
assert_eq!(recipients.len(), 1);
|
||||||
|
let recipient = recipients[0].as_deref().expect("a named recipient");
|
||||||
|
// Read from the locked key: no passphrase is needed to name it.
|
||||||
|
assert!(
|
||||||
|
key_ids(&node.locked_armored)
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.any(|id| id == recipient)
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!key_ids(&other.locked_armored)
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.any(|id| id == recipient)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn from_armored_rejects_a_wrong_passphrase() {
|
fn from_armored_rejects_a_wrong_passphrase() {
|
||||||
// `from_armored` doubles as the passphrase check, so a key that does not
|
// `from_armored` doubles as the passphrase check, so a key that does not
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
//! PGP message decryption helpers.
|
//! PGP message decryption helpers.
|
||||||
|
|
||||||
use pgp::composed::{Message, SignedSecretKey};
|
use pgp::composed::{Esk, Message, SignedSecretKey};
|
||||||
use pgp::types::Password;
|
use pgp::types::Password;
|
||||||
|
|
||||||
use super::errors::CryptoError;
|
use super::errors::CryptoError;
|
||||||
@@ -81,6 +81,35 @@ pub fn decrypt_armored_any(
|
|||||||
Err(last_err)
|
Err(last_err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The keys an armored message's session key is encrypted to, as lowercase
|
||||||
|
/// hex: the key ID of a v3 PKESK, the fingerprint of a v6 one. A hidden
|
||||||
|
/// recipient (a wildcard key ID, or a v6 PKESK without a fingerprint) is
|
||||||
|
/// `None`. Only the packet headers are read, so no key is needed.
|
||||||
|
///
|
||||||
|
/// Matched against [`key_ids`](super::key_ids), this tells which key a
|
||||||
|
/// message that fails with "missing key" was meant for.
|
||||||
|
pub fn recipients(armored: &str) -> Result<Vec<Option<String>>, CryptoError> {
|
||||||
|
let (message, _headers) =
|
||||||
|
Message::from_string(armored).map_err(|e| CryptoError::Parse(format!("message: {e}")))?;
|
||||||
|
let Message::Encrypted { esk, .. } = &message else {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
};
|
||||||
|
Ok(esk
|
||||||
|
.iter()
|
||||||
|
.filter_map(|esk| match esk {
|
||||||
|
Esk::PublicKeyEncryptedSessionKey(pkesk) => Some(match pkesk.fingerprint() {
|
||||||
|
Ok(fingerprint) => fingerprint.map(|f| format!("{f:x}")),
|
||||||
|
Err(_) => pkesk
|
||||||
|
.id()
|
||||||
|
.ok()
|
||||||
|
.filter(|id| !id.is_wildcard())
|
||||||
|
.map(|id| id.to_string()),
|
||||||
|
}),
|
||||||
|
Esk::SymKeyEncryptedSessionKey(_) => None,
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
/// Drain a decrypted message to bytes, transparently decompressing if needed.
|
/// Drain a decrypted message to bytes, transparently decompressing if needed.
|
||||||
pub(crate) fn read_plaintext(mut message: Message<'_>) -> Result<Vec<u8>, CryptoError> {
|
pub(crate) fn read_plaintext(mut message: Message<'_>) -> Result<Vec<u8>, CryptoError> {
|
||||||
while message.is_compressed() {
|
while message.is_compressed() {
|
||||||
@@ -102,6 +131,12 @@ mod tests {
|
|||||||
use pgp::crypto::sym::SymmetricKeyAlgorithm;
|
use pgp::crypto::sym::SymmetricKeyAlgorithm;
|
||||||
use pgp::types::{Password, StringToKey};
|
use pgp::types::{Password, StringToKey};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_password_encrypted_message_has_no_key_recipients() {
|
||||||
|
let message = encrypt_armored_with_password(b"secret", b"password");
|
||||||
|
assert!(super::recipients(&message).unwrap().is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
/// Build the shape Proton's `SharePassphrase` has: an armored, symmetrically
|
/// Build the shape Proton's `SharePassphrase` has: an armored, symmetrically
|
||||||
/// encrypted message whose passphrase is the bcrypt-derived key password.
|
/// encrypted message whose passphrase is the bcrypt-derived key password.
|
||||||
fn encrypt_armored_with_password(plaintext: &[u8], password: &[u8]) -> String {
|
fn encrypt_armored_with_password(plaintext: &[u8], password: &[u8]) -> String {
|
||||||
|
|||||||
Reference in new issue
Block a user