use crate::core::error::{PaserkError, PaserkResult};
pub const PBKDF2_SALT_SIZE: usize = 32;
pub const AES_CTR_NONCE_SIZE: usize = 16;
pub const PBKW_K1K3_TAG_SIZE: usize = 48;
pub type PbkwLocalOutputK1K3 = (
[u8; PBKDF2_SALT_SIZE],
[u8; AES_CTR_NONCE_SIZE],
[u8; 32],
[u8; PBKW_K1K3_TAG_SIZE],
);
pub type PbkwSecretOutputK3 = (
[u8; PBKDF2_SALT_SIZE],
[u8; AES_CTR_NONCE_SIZE],
[u8; 48],
[u8; PBKW_K1K3_TAG_SIZE],
);
const PBKW_EK_DOMAIN: &[u8] = b"paserk-wrap.pie-local";
const PBKW_AK_SUFFIX: &[u8] = b"auth-key-for-tag";
#[derive(Debug, Clone, Copy)]
pub struct Pbkdf2Params {
pub iterations: u32,
}
impl Default for Pbkdf2Params {
fn default() -> Self {
Self::moderate()
}
}
impl Pbkdf2Params {
#[must_use]
pub const fn interactive() -> Self {
Self {
iterations: 100_000,
}
}
#[must_use]
pub const fn moderate() -> Self {
Self {
iterations: 310_000,
}
}
#[must_use]
pub const fn sensitive() -> Self {
Self {
iterations: 600_000,
}
}
}
#[cfg(any(feature = "k1-insecure", feature = "k3"))]
fn derive_keys_k1k3(
password: &[u8],
salt: &[u8; PBKDF2_SALT_SIZE],
iterations: u32,
domain: &[u8],
) -> PaserkResult<([u8; 32], [u8; 48])> {
use hmac::{Hmac, Mac};
use sha2::Sha384;
let mut psk = [0u8; 32];
pbkdf2::pbkdf2::<Hmac<Sha384>>(password, salt, iterations, &mut psk)
.map_err(|_| PaserkError::KeyDerivationFailed)?;
let mut ek_mac =
<Hmac<Sha384> as Mac>::new_from_slice(&psk).map_err(|_| PaserkError::CryptoError)?;
ek_mac.update(domain);
let ek_result = ek_mac.finalize().into_bytes();
let mut encryption_key = [0u8; 32];
encryption_key.copy_from_slice(&ek_result[..32]);
let mut ak_mac =
<Hmac<Sha384> as Mac>::new_from_slice(&psk).map_err(|_| PaserkError::CryptoError)?;
ak_mac.update(domain);
ak_mac.update(PBKW_AK_SUFFIX);
let auth_key: [u8; 48] = ak_mac.finalize().into_bytes().into();
zeroize::Zeroize::zeroize(&mut psk);
Ok((encryption_key, auth_key))
}
#[cfg(any(feature = "k1-insecure", feature = "k3"))]
fn aes_ctr_encrypt(key: &[u8; 32], nonce: &[u8; AES_CTR_NONCE_SIZE], data: &mut [u8]) {
use aes::cipher::{KeyIvInit, StreamCipher};
use ctr::Ctr128BE;
type Aes256Ctr = Ctr128BE<aes::Aes256>;
let mut cipher = Aes256Ctr::new(key.into(), nonce.into());
cipher.apply_keystream(data);
}
#[cfg(any(feature = "k1-insecure", feature = "k3"))]
fn compute_tag_k1k3(
auth_key: &[u8; 48],
header: &str,
salt: &[u8],
nonce: &[u8],
ciphertext: &[u8],
) -> PaserkResult<[u8; PBKW_K1K3_TAG_SIZE]> {
use hmac::{Hmac, Mac};
use sha2::Sha384;
let mut tag_mac =
<Hmac<Sha384> as Mac>::new_from_slice(auth_key).map_err(|_| PaserkError::CryptoError)?;
tag_mac.update(header.as_bytes());
tag_mac.update(salt);
tag_mac.update(nonce);
tag_mac.update(ciphertext);
Ok(tag_mac.finalize().into_bytes().into())
}
#[cfg(any(feature = "k1-insecure", feature = "k3"))]
pub fn pbkw_wrap_local_k1k3(
plaintext_key: &[u8; 32],
password: &[u8],
params: Pbkdf2Params,
header: &str,
) -> PaserkResult<PbkwLocalOutputK1K3> {
use rand_core::{OsRng, TryRngCore};
let mut salt = [0u8; PBKDF2_SALT_SIZE];
let mut nonce = [0u8; AES_CTR_NONCE_SIZE];
OsRng
.try_fill_bytes(&mut salt)
.map_err(|_| PaserkError::CryptoError)?;
OsRng
.try_fill_bytes(&mut nonce)
.map_err(|_| PaserkError::CryptoError)?;
let (mut encryption_key, mut auth_key) =
derive_keys_k1k3(password, &salt, params.iterations, PBKW_EK_DOMAIN)?;
let mut ciphertext = *plaintext_key;
aes_ctr_encrypt(&encryption_key, &nonce, &mut ciphertext);
let tag = compute_tag_k1k3(&auth_key, header, &salt, &nonce, &ciphertext)?;
zeroize::Zeroize::zeroize(&mut encryption_key);
zeroize::Zeroize::zeroize(&mut auth_key);
Ok((salt, nonce, ciphertext, tag))
}
#[cfg(any(feature = "k1-insecure", feature = "k3"))]
pub fn pbkw_unwrap_local_k1k3(
salt: &[u8; PBKDF2_SALT_SIZE],
nonce: &[u8; AES_CTR_NONCE_SIZE],
ciphertext: &[u8; 32],
tag: &[u8; PBKW_K1K3_TAG_SIZE],
password: &[u8],
params: Pbkdf2Params,
header: &str,
) -> PaserkResult<[u8; 32]> {
use subtle::ConstantTimeEq;
let (mut encryption_key, mut auth_key) =
derive_keys_k1k3(password, salt, params.iterations, PBKW_EK_DOMAIN)?;
let computed_tag = compute_tag_k1k3(&auth_key, header, salt, nonce, ciphertext)?;
zeroize::Zeroize::zeroize(&mut auth_key);
if computed_tag.ct_eq(tag).into() {
let mut plaintext = *ciphertext;
aes_ctr_encrypt(&encryption_key, nonce, &mut plaintext);
zeroize::Zeroize::zeroize(&mut encryption_key);
Ok(plaintext)
} else {
zeroize::Zeroize::zeroize(&mut encryption_key);
Err(PaserkError::AuthenticationFailed)
}
}
#[cfg(feature = "k3")]
pub fn pbkw_wrap_secret_k3(
plaintext_key: &[u8; 48],
password: &[u8],
params: Pbkdf2Params,
header: &str,
) -> PaserkResult<PbkwSecretOutputK3> {
use rand_core::{OsRng, TryRngCore};
const PBKW_SECRET_DOMAIN: &[u8] = b"paserk-wrap.pie-secret";
let mut salt = [0u8; PBKDF2_SALT_SIZE];
let mut nonce = [0u8; AES_CTR_NONCE_SIZE];
OsRng
.try_fill_bytes(&mut salt)
.map_err(|_| PaserkError::CryptoError)?;
OsRng
.try_fill_bytes(&mut nonce)
.map_err(|_| PaserkError::CryptoError)?;
let (mut encryption_key, mut auth_key) =
derive_keys_k1k3(password, &salt, params.iterations, PBKW_SECRET_DOMAIN)?;
let mut ciphertext = *plaintext_key;
aes_ctr_encrypt(&encryption_key, &nonce, &mut ciphertext);
let tag = compute_tag_k1k3(&auth_key, header, &salt, &nonce, &ciphertext)?;
zeroize::Zeroize::zeroize(&mut encryption_key);
zeroize::Zeroize::zeroize(&mut auth_key);
Ok((salt, nonce, ciphertext, tag))
}
#[cfg(feature = "k3")]
pub fn pbkw_unwrap_secret_k3(
salt: &[u8; PBKDF2_SALT_SIZE],
nonce: &[u8; AES_CTR_NONCE_SIZE],
ciphertext: &[u8; 48],
tag: &[u8; PBKW_K1K3_TAG_SIZE],
password: &[u8],
params: Pbkdf2Params,
header: &str,
) -> PaserkResult<[u8; 48]> {
use subtle::ConstantTimeEq;
const PBKW_SECRET_DOMAIN: &[u8] = b"paserk-wrap.pie-secret";
let (mut encryption_key, mut auth_key) =
derive_keys_k1k3(password, salt, params.iterations, PBKW_SECRET_DOMAIN)?;
let computed_tag = compute_tag_k1k3(&auth_key, header, salt, nonce, ciphertext)?;
zeroize::Zeroize::zeroize(&mut auth_key);
if computed_tag.ct_eq(tag).into() {
let mut plaintext = *ciphertext;
aes_ctr_encrypt(&encryption_key, nonce, &mut plaintext);
zeroize::Zeroize::zeroize(&mut encryption_key);
Ok(plaintext)
} else {
zeroize::Zeroize::zeroize(&mut encryption_key);
Err(PaserkError::AuthenticationFailed)
}
}
#[cfg(test)]
#[allow(deprecated)]
mod tests {
use super::*;
fn test_params() -> Pbkdf2Params {
Pbkdf2Params { iterations: 1000 }
}
#[test]
#[cfg(feature = "k3")]
fn test_pbkw_wrap_unwrap_local_k3_roundtrip() -> PaserkResult<()> {
let plaintext_key = [0x13u8; 32];
let password = b"hunter2";
let params = test_params();
let header = "k3.local-pw.";
let (salt, nonce, ciphertext, tag) =
pbkw_wrap_local_k1k3(&plaintext_key, password, params, header)?;
assert_ne!(ciphertext, plaintext_key);
let unwrapped =
pbkw_unwrap_local_k1k3(&salt, &nonce, &ciphertext, &tag, password, params, header)?;
assert_eq!(unwrapped, plaintext_key);
Ok(())
}
#[test]
#[cfg(feature = "k1-insecure")]
fn test_pbkw_wrap_unwrap_local_k1_roundtrip() -> PaserkResult<()> {
let plaintext_key = [0x13u8; 32];
let password = b"hunter2";
let params = test_params();
let header = "k1.local-pw.";
let (salt, nonce, ciphertext, tag) =
pbkw_wrap_local_k1k3(&plaintext_key, password, params, header)?;
assert_ne!(ciphertext, plaintext_key);
let unwrapped =
pbkw_unwrap_local_k1k3(&salt, &nonce, &ciphertext, &tag, password, params, header)?;
assert_eq!(unwrapped, plaintext_key);
Ok(())
}
#[test]
#[cfg(feature = "k3")]
fn test_pbkw_wrap_unwrap_secret_k3_roundtrip() -> PaserkResult<()> {
let plaintext_key = [0x13u8; 48];
let password = b"hunter2";
let params = test_params();
let header = "k3.secret-pw.";
let (salt, nonce, ciphertext, tag) =
pbkw_wrap_secret_k3(&plaintext_key, password, params, header)?;
assert_ne!(ciphertext, plaintext_key);
let unwrapped =
pbkw_unwrap_secret_k3(&salt, &nonce, &ciphertext, &tag, password, params, header)?;
assert_eq!(unwrapped, plaintext_key);
Ok(())
}
#[test]
#[cfg(feature = "k3")]
fn test_pbkw_unwrap_wrong_password() -> PaserkResult<()> {
let plaintext_key = [0x13u8; 32];
let password = b"hunter2";
let wrong_password = b"hunter3";
let params = test_params();
let header = "k3.local-pw.";
let (salt, nonce, ciphertext, tag) =
pbkw_wrap_local_k1k3(&plaintext_key, password, params, header)?;
let result = pbkw_unwrap_local_k1k3(
&salt,
&nonce,
&ciphertext,
&tag,
wrong_password,
params,
header,
);
assert!(matches!(result, Err(PaserkError::AuthenticationFailed)));
Ok(())
}
#[test]
#[cfg(feature = "k3")]
fn test_pbkw_unwrap_modified_tag() -> PaserkResult<()> {
let plaintext_key = [0x13u8; 32];
let password = b"hunter2";
let params = test_params();
let header = "k3.local-pw.";
let (salt, nonce, ciphertext, mut tag) =
pbkw_wrap_local_k1k3(&plaintext_key, password, params, header)?;
tag[0] ^= 0xff;
let result =
pbkw_unwrap_local_k1k3(&salt, &nonce, &ciphertext, &tag, password, params, header);
assert!(matches!(result, Err(PaserkError::AuthenticationFailed)));
Ok(())
}
#[test]
fn test_pbkdf2_params_presets() {
let interactive = Pbkdf2Params::interactive();
assert_eq!(interactive.iterations, 100_000);
let moderate = Pbkdf2Params::moderate();
assert_eq!(moderate.iterations, 310_000);
let sensitive = Pbkdf2Params::sensitive();
assert_eq!(sensitive.iterations, 600_000);
}
}