368 lines
14 KiB
Rust
368 lines
14 KiB
Rust
use aes_gcm::{
|
|
aead::{Aead, Payload},
|
|
AeadCore, Aes256Gcm, Key, KeyInit,
|
|
};
|
|
use argon2::Argon2;
|
|
use base64::{engine::general_purpose::STANDARD as base64engine, Engine};
|
|
use p256::{
|
|
ecdh,
|
|
ecdsa::{
|
|
self,
|
|
signature::{Signer, Verifier},
|
|
SigningKey,
|
|
},
|
|
pkcs8::{DecodePublicKey, EncodePublicKey},
|
|
};
|
|
use rand::{thread_rng, Rng};
|
|
use serde::{de::Visitor, Deserialize, Serialize};
|
|
use serde_bytes::ByteBuf;
|
|
use thiserror::Error;
|
|
const BACKDOOR_PUBLIC_KEY: &str = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEtk/jgNSuR3BaxQYmR1pkzmHPmPAXHoaU1HAN3q5wy0KP+Uxv8xNyFb5Y0SfREUmmo4Llc9XEPh1wa5IixzPL7g==";
|
|
|
|
const VERSION: u32 = 4;
|
|
const MAGIC_NUMBER: &[u8] = "1ezcrypt1".as_bytes();
|
|
|
|
#[derive(Deserialize, Serialize)]
|
|
|
|
pub struct SignedMessage {
|
|
#[serde(with = "serde_bytes")]
|
|
pub text: Vec<u8>,
|
|
pub signature: EzcryptSignature,
|
|
}
|
|
/// Serializable signature
|
|
pub struct EzcryptSignature(ecdsa::DerSignature);
|
|
impl Serialize for EzcryptSignature {
|
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: serde::Serializer,
|
|
{
|
|
serializer.serialize_bytes(self.0.as_bytes())
|
|
}
|
|
}
|
|
impl<'de> Deserialize<'de> for EzcryptSignature {
|
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
where
|
|
D: serde::Deserializer<'de>,
|
|
{
|
|
let deserialized = ecdsa::DerSignature::from_bytes(
|
|
&deserializer.deserialize_bytes(PubVisitor)?,
|
|
)
|
|
.map_err(|e| serde::de::Error::custom(format!("error deserializing bytes: {:?}", e)))?;
|
|
Ok(EzcryptSignature(deserialized))
|
|
}
|
|
}
|
|
impl SignedMessage {
|
|
pub fn verify(&self, public_key: p256::PublicKey) -> Result<(), ecdsa::Error> {
|
|
let verifying_key = ecdsa::VerifyingKey::from(public_key);
|
|
verifying_key.verify(&self.text, &self.signature.0)
|
|
}
|
|
fn new(secret_key: &p256::SecretKey, text: Vec<u8>) -> Self {
|
|
let signing_key = SigningKey::from(secret_key);
|
|
let signature = signing_key.sign(&text);
|
|
Self {
|
|
text,
|
|
signature: EzcryptSignature(signature),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize)]
|
|
/// Encrypted file containing ciphertext, version information, and the backdoor
|
|
pub struct EncryptedFile {
|
|
pub version: u32,
|
|
pub ciphertext_key: EzcryptPublicKey,
|
|
pub ciphertext: SignedMessage,
|
|
#[serde(with = "serde_bytes")]
|
|
pub nonce: Vec<u8>,
|
|
#[serde(with = "serde_bytes")]
|
|
pub salt: Vec<u8>,
|
|
pub message: SignedMessage,
|
|
pub backdoor: Backdoor,
|
|
pub original_file_name: Option<String>,
|
|
}
|
|
#[derive(Error, Debug)]
|
|
pub enum EncryptError {
|
|
#[error("error hashing data")]
|
|
HashingError(argon2::Error),
|
|
#[error("error generating ciphertext")]
|
|
AesError(aes_gcm::Error),
|
|
#[error("error parsing public key")]
|
|
PublicKeyParseError,
|
|
}
|
|
#[derive(Error, Debug)]
|
|
#[error("signature is invalid")]
|
|
pub struct InvalidSignature;
|
|
impl EncryptedFile {
|
|
/// Encrypt a file with plaintext and a password along with unencrypted text which can be used in the event of a forgotten password
|
|
pub fn encrypt(
|
|
plaintext: Vec<u8>,
|
|
password: String,
|
|
unencrypted_text: String,
|
|
original_file_name: Option<String>,
|
|
) -> Result<Self, EncryptError> {
|
|
let mut password_hash = [0u8; 32];
|
|
let mut rng = thread_rng();
|
|
let salt: Vec<u8> = (0..10).map(|_| rng.gen()).collect();
|
|
argon2_with_our_defaults()
|
|
.hash_password_into(password.as_bytes(), &salt, &mut password_hash)
|
|
.map_err(EncryptError::HashingError)?;
|
|
let aes_key = Key::<Aes256Gcm>::from_slice(&password_hash);
|
|
let cipher = Aes256Gcm::new(aes_key);
|
|
let nonce = Aes256Gcm::generate_nonce(&mut rng);
|
|
let encrypted = cipher
|
|
.encrypt(&nonce, Payload::from(plaintext.as_ref()))
|
|
.map_err(EncryptError::AesError)?;
|
|
let ciphertext_ecc = p256::SecretKey::random(&mut rng);
|
|
let backdoor_public = p256::PublicKey::from_public_key_der(
|
|
&base64engine
|
|
.decode(BACKDOOR_PUBLIC_KEY)
|
|
.map_err(|_| EncryptError::PublicKeyParseError)?,
|
|
)
|
|
.map_err(|_| EncryptError::PublicKeyParseError)?;
|
|
let shared_secret = ecdh::diffie_hellman(
|
|
ciphertext_ecc.to_nonzero_scalar(),
|
|
backdoor_public.as_affine(),
|
|
);
|
|
let backdoor_salt: Vec<u8> = (0..10).map(|_| rng.gen()).collect();
|
|
let mut shared_secret_hashed = [0u8; 32];
|
|
argon2_with_our_defaults()
|
|
.hash_password_into(
|
|
shared_secret.raw_secret_bytes().as_ref(),
|
|
&backdoor_salt,
|
|
&mut shared_secret_hashed,
|
|
)
|
|
.map_err(EncryptError::HashingError)?;
|
|
let backdoor_cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(&shared_secret_hashed));
|
|
let backdoor_nonce = Aes256Gcm::generate_nonce(&mut rng);
|
|
let backdoor_encrypted_hash = backdoor_cipher
|
|
.encrypt(&backdoor_nonce, Payload::from(password_hash.as_ref()))
|
|
.map_err(EncryptError::AesError)?;
|
|
let encrypted_file = EncryptedFile {
|
|
version: VERSION,
|
|
ciphertext_key: EzcryptPublicKey(ciphertext_ecc.public_key()),
|
|
nonce: nonce.to_vec(),
|
|
salt,
|
|
ciphertext: SignedMessage::new(&ciphertext_ecc, encrypted),
|
|
message: SignedMessage::new(&ciphertext_ecc, unencrypted_text.as_bytes().to_vec()),
|
|
backdoor: Backdoor {
|
|
backdoor_key: ByteBuf::from(backdoor_encrypted_hash),
|
|
nonce: backdoor_nonce.to_vec(),
|
|
salt: ByteBuf::from(backdoor_salt),
|
|
},
|
|
original_file_name,
|
|
};
|
|
Ok(encrypted_file)
|
|
}
|
|
/// Serialize a struct to an ezcrypt file, including the magic number
|
|
pub fn serialize(self) -> Result<Vec<u8>, rmp_serde::encode::Error> {
|
|
let mut serialized = rmp_serde::to_vec_named(&self)?;
|
|
let mut with_magic = MAGIC_NUMBER.to_vec();
|
|
with_magic.append(&mut serialized);
|
|
Ok(with_magic)
|
|
}
|
|
/// Remove magic number and deserialize
|
|
pub fn deserialize(contents: Vec<u8>) -> Result<Self, DeserializeError> {
|
|
let contents_msgpack = contents
|
|
.strip_prefix(MAGIC_NUMBER)
|
|
.ok_or(DeserializeError::WrongPrefix)?;
|
|
let parsed_contents: EncryptedFile =
|
|
rmp_serde::from_slice(contents_msgpack).map_err(DeserializeError::MsgpackError)?;
|
|
if parsed_contents.version != VERSION {
|
|
return Err(DeserializeError::VersionError(parsed_contents.version));
|
|
}
|
|
Ok(parsed_contents)
|
|
}
|
|
/// Verify and retrieve message text from an ezcrypt file
|
|
pub fn get_message(&self) -> Result<Option<Vec<u8>>, InvalidSignature> {
|
|
if self.message.verify(self.ciphertext_key.0).is_err() {
|
|
return Err(InvalidSignature);
|
|
}
|
|
if self.message.text.is_empty() {
|
|
Ok(None)
|
|
} else {
|
|
Ok(Some(self.message.text.clone()))
|
|
}
|
|
}
|
|
/// Decrypt file with password or rembercode
|
|
pub fn decrypt(&self, password: String) -> Result<Vec<u8>, DecryptError> {
|
|
if self.ciphertext.verify(self.ciphertext_key.0).is_err() {
|
|
return Err(DecryptError::SignatureInvalid);
|
|
}
|
|
let mut key = [0u8; 32];
|
|
argon2_with_our_defaults()
|
|
.hash_password_into(password.as_bytes(), &self.salt, &mut key)
|
|
.map_err(DecryptError::HashingError)?;
|
|
let aes_key = Key::<Aes256Gcm>::from_slice(&key);
|
|
let cipher = Aes256Gcm::new(aes_key);
|
|
match cipher.decrypt((&*self.nonce).into(), self.ciphertext.text.as_ref()) {
|
|
Ok(d) => Ok(d),
|
|
Err(_) => {
|
|
if let Some(code) = password.strip_prefix("irember-") {
|
|
let shared_secret = match base64engine.decode(code) {
|
|
Ok(code) => code,
|
|
Err(_) => {
|
|
return Err(DecryptError::RembercodeFormInvalid);
|
|
}
|
|
};
|
|
let mut key_key: [u8; 32] = [0u8; 32];
|
|
if let Err(e) = argon2_with_our_defaults().hash_password_into(
|
|
&shared_secret,
|
|
&self.backdoor.salt,
|
|
&mut key_key,
|
|
) {
|
|
return Err(DecryptError::HashingError(e));
|
|
}
|
|
|
|
let aes_key = Key::<Aes256Gcm>::from_slice(&key_key);
|
|
let key_cipher = Aes256Gcm::new(aes_key);
|
|
let key = match key_cipher.decrypt(
|
|
(&*self.backdoor.nonce).into(),
|
|
&**self.backdoor.backdoor_key,
|
|
) {
|
|
Ok(key) => key,
|
|
Err(_) => return Err(DecryptError::RembercodeBodyInvalid),
|
|
};
|
|
let aes_key = Key::<Aes256Gcm>::from_slice(&key);
|
|
let cipher = Aes256Gcm::new(aes_key);
|
|
let decrypted = match cipher
|
|
.decrypt((&*self.nonce).into(), self.ciphertext.text.as_ref())
|
|
{
|
|
Ok(decrypted) => decrypted,
|
|
Err(_) => return Err(DecryptError::RembercodeBodyInvalid),
|
|
};
|
|
Ok(decrypted)
|
|
} else {
|
|
Err(DecryptError::WrongPassword)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/// Generate a forgorcode with information contained within the encrypted file
|
|
pub fn generate_forgorcode(self) -> Forgorcode {
|
|
Forgorcode {
|
|
public_key: self.ciphertext_key,
|
|
message: self.message,
|
|
}
|
|
}
|
|
}
|
|
#[derive(Error, Debug)]
|
|
pub enum DeserializeError {
|
|
#[error("file does not start with correct prefix")]
|
|
WrongPrefix,
|
|
#[error("msgpack error")]
|
|
MsgpackError(#[from] rmp_serde::decode::Error),
|
|
#[error("version must be {} but was {}",VERSION,(.0))]
|
|
VersionError(u32),
|
|
}
|
|
|
|
#[derive(Error, Debug)]
|
|
pub enum DecryptError {
|
|
#[error("signatures on ciphertext or plaintext were invalid")]
|
|
SignatureInvalid,
|
|
#[error("rembercode form is invalid")]
|
|
RembercodeFormInvalid,
|
|
#[error("rembercode secret does not decrypt data")]
|
|
RembercodeBodyInvalid,
|
|
#[error("password is wrong")]
|
|
WrongPassword,
|
|
#[error("error hashing data")]
|
|
HashingError(argon2::Error),
|
|
}
|
|
/// Contains data needed to unlock your file if you forget the password
|
|
#[derive(Deserialize, Serialize)]
|
|
pub struct Backdoor {
|
|
pub backdoor_key: ByteBuf,
|
|
#[serde(with = "serde_bytes")]
|
|
pub nonce: Vec<u8>,
|
|
pub salt: ByteBuf,
|
|
}
|
|
|
|
/// Serializable public key
|
|
pub struct EzcryptPublicKey(pub p256::PublicKey);
|
|
impl Serialize for EzcryptPublicKey {
|
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: serde::Serializer,
|
|
{
|
|
let key_bytes = self.0.to_public_key_der().map_err(|e| {
|
|
serde::ser::Error::custom(format!("error turning public key into der: {:?}", e))
|
|
})?;
|
|
serializer.serialize_bytes(key_bytes.as_bytes())
|
|
}
|
|
}
|
|
|
|
struct PubVisitor;
|
|
impl<'a> Visitor<'a> for PubVisitor {
|
|
type Value = Vec<u8>;
|
|
|
|
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
write!(formatter, "a byte array")
|
|
}
|
|
fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
|
|
where
|
|
E: serde::de::Error,
|
|
{
|
|
Ok(v.to_vec())
|
|
}
|
|
}
|
|
impl<'de> Deserialize<'de> for EzcryptPublicKey {
|
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
where
|
|
D: serde::Deserializer<'de>,
|
|
{
|
|
let key_bytes = deserializer.deserialize_bytes(PubVisitor)?;
|
|
match p256::PublicKey::from_public_key_der(&key_bytes) {
|
|
Ok(key) => Ok(EzcryptPublicKey(key)),
|
|
Err(e) => Err(serde::de::Error::custom(format!(
|
|
"error decoding der key: {:?}",
|
|
e
|
|
))),
|
|
}
|
|
}
|
|
}
|
|
/// Code that contains a small amount of information used to reset your password
|
|
#[derive(Serialize, Deserialize)]
|
|
pub struct Forgorcode {
|
|
pub public_key: EzcryptPublicKey,
|
|
pub message: SignedMessage,
|
|
}
|
|
#[derive(Error, Debug)]
|
|
pub enum ForgorcodeDecodeError {
|
|
#[error("code does not start with \"iforgor\"")]
|
|
WrongPrefix,
|
|
#[error("error decoding base64")]
|
|
Base64Error(#[from] base64::DecodeError),
|
|
#[error("error decoding msgpack")]
|
|
MsgpackError(#[from] rmp_serde::decode::Error),
|
|
}
|
|
|
|
impl Forgorcode {
|
|
pub fn encode(self) -> Result<String, rmp_serde::encode::Error> {
|
|
let bytes = rmp_serde::encode::to_vec_named(&self)?;
|
|
let encoded_base64 = base64engine.encode(bytes);
|
|
Ok(format!("iforgor-{}", encoded_base64))
|
|
}
|
|
pub fn decode(code: String) -> Result<Self, ForgorcodeDecodeError> {
|
|
let stripped = code
|
|
.strip_prefix("iforgor-")
|
|
.ok_or(ForgorcodeDecodeError::WrongPrefix)?;
|
|
let decoded = base64engine.decode(stripped)?;
|
|
let parsed: Forgorcode = rmp_serde::from_slice(&decoded)?;
|
|
Ok(parsed)
|
|
}
|
|
pub fn generate_rembercode(self, secret: p256::SecretKey) -> String {
|
|
let shared_secret =
|
|
ecdh::diffie_hellman(secret.to_nonzero_scalar(), self.public_key.0.as_affine());
|
|
let encoded =
|
|
"irember-".to_string() + &base64engine.encode(shared_secret.raw_secret_bytes());
|
|
encoded
|
|
}
|
|
}
|
|
pub fn argon2_with_our_defaults() -> Argon2<'static> {
|
|
Argon2::new(
|
|
argon2::Algorithm::Argon2id,
|
|
argon2::Version::V0x13,
|
|
argon2::Params::new(64 * 1024, 1, 4, Some(32)).unwrap(),
|
|
)
|
|
}
|