mirror of
https://github.com/signalapp/libsignal.git
synced 2026-04-25 17:25:18 +02:00
246 lines
8.8 KiB
Rust
246 lines
8.8 KiB
Rust
//
|
|
// Copyright 2023 Signal Messenger, LLC.
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
//! A library for hashing pins
|
|
//!
|
|
//! This library provides two pin hashing mechanisms:
|
|
//! 1. Transforming a pin to be suitable for use with a Secure Value Recovery service. The pin
|
|
//! is hashed with Argon2 into 64 bytes. One half of these bytes are provided to the service
|
|
//! as a password protecting some arbitrary data. The other half is used as an encryption key
|
|
//! for that data. See `PinHash`
|
|
//! 2. Creating a [PHC-string encoded](https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md#specification)
|
|
//! password hash of the pin that can be stored locally and validated against the pin later.
|
|
//!
|
|
//! In either case, all pins are UTF-8 encoded bytes that must be normalized *before* being provided
|
|
//! to this library. Normalizing a string pin requires the following steps:
|
|
//! 1. The string should be trimmed for leading and trailing whitespace.
|
|
//! 2. If the whole string consists of digits, then non-arabic digits must be replaced with their
|
|
//! arabic 0-9 equivalents.
|
|
//! 3. The string must then be [NFKD normalized](https://unicode.org/reports/tr15/#Norm_Forms)
|
|
//!
|
|
|
|
use argon2::password_hash::{Salt, SaltString, rand_core};
|
|
use argon2::{
|
|
Algorithm, Argon2, ParamsBuilder, PasswordHash, PasswordHasher, PasswordVerifier, Version,
|
|
};
|
|
use hkdf::Hkdf;
|
|
use sha2::Sha256;
|
|
|
|
use crate::error::Result;
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct PinHash {
|
|
/// A key that can be used to encrypt or decrypt values before uploading them to a secure store.
|
|
/// The 32 byte prefix of the 64 byte hashed pin.
|
|
pub encryption_key: [u8; 32],
|
|
|
|
/// A secret that can be used to access a value in a secure store. The 32 byte suffix of
|
|
/// the 64 byte hashed pin.
|
|
pub access_key: [u8; 32],
|
|
}
|
|
|
|
impl PinHash {
|
|
/// Hash an arbitrary pin into an encryption key and access key that can be used to interact
|
|
/// with a Secure Value Recovery service.
|
|
///
|
|
/// # Arguments
|
|
/// * `pin` - UTF-8 encoding of the pin. The pin *must* be normalized first.
|
|
/// * `salt` - An arbitrary 32 byte value that should be unique to the user
|
|
pub fn create(pin: &[u8], salt: &[u8; 32]) -> Result<PinHash> {
|
|
let hasher = Argon2::new(
|
|
Algorithm::Argon2id,
|
|
Version::V0x13,
|
|
ParamsBuilder::new()
|
|
.m_cost(1024 * 16) // 16 MiB
|
|
.p_cost(1)
|
|
.t_cost(32)
|
|
.output_len(64)
|
|
.build()
|
|
.expect("valid params"),
|
|
);
|
|
let mut output_key_material = [0u8; 64];
|
|
hasher.hash_password_into(pin, salt, &mut output_key_material)?;
|
|
Ok(PinHash {
|
|
encryption_key: output_key_material[..32]
|
|
.try_into()
|
|
.expect("target length 32"),
|
|
access_key: output_key_material[32..]
|
|
.try_into()
|
|
.expect("target length 32"),
|
|
})
|
|
}
|
|
|
|
/// Create a salt from a username and the group id of the SVR service. This
|
|
/// function should always be used to create pin salts for SVR2.
|
|
///
|
|
/// # Arguments
|
|
/// * `username` - The Basic Auth username credential retrieved from the chat service and used to authenticate with the SVR service
|
|
/// * `group_id` - The attested group id returned by the SVR service
|
|
pub fn make_salt(username: &str, group_id: u64) -> [u8; 32] {
|
|
let mut out = [0u8; 32];
|
|
Hkdf::<Sha256>::new(Some(&group_id.to_be_bytes()), username.as_bytes())
|
|
.expand(&[], &mut out)
|
|
.expect("should expand");
|
|
out
|
|
}
|
|
}
|
|
|
|
/// Create a PHC encoded password hash string. This string may be verified later with
|
|
/// `verify_local_pin_hash`.
|
|
///
|
|
/// # Arguments
|
|
/// * `pin` - UTF-8 encoding of the pin. The pin *must* be normalized first.
|
|
pub fn local_pin_hash(pin: &[u8]) -> Result<String> {
|
|
static_assertions::const_assert_eq!(Salt::RECOMMENDED_LENGTH, 16);
|
|
let salt = SaltString::generate(&mut rand_core::OsRng);
|
|
local_pin_hash_with_salt(pin, &salt)
|
|
}
|
|
|
|
fn local_pin_hash_with_salt<'a>(pin: &[u8], salt: impl Into<Salt<'a>>) -> Result<String> {
|
|
let hasher = Argon2::new(
|
|
Algorithm::Argon2i,
|
|
Version::V0x13,
|
|
ParamsBuilder::new()
|
|
.m_cost(512)
|
|
.p_cost(1)
|
|
.t_cost(64)
|
|
.output_len(32)
|
|
.build()
|
|
.expect("valid params"),
|
|
);
|
|
let hash = hasher.hash_password(pin, salt)?;
|
|
Ok(hash.to_string())
|
|
}
|
|
|
|
/// Verify an encoded password hash against a pin
|
|
///
|
|
/// # Arguments
|
|
/// * `pin` - UTF-8 encoding of the pin. The pin *must* be normalized first.
|
|
/// * `encoded_hash` - A PHC-string formatted representation of the hash, as returned by `local_pin_hash`
|
|
pub fn verify_local_pin_hash(encoded_hash: &str, pin: &[u8]) -> Result<bool> {
|
|
let parsed = PasswordHash::new(encoded_hash)?;
|
|
Ok(Argon2::default().verify_password(pin, &parsed).is_ok())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use const_str::hex;
|
|
use hmac::{Hmac, Mac};
|
|
use sha2::Sha256;
|
|
|
|
use super::*;
|
|
use crate::hash::{PinHash, local_pin_hash, verify_local_pin_hash};
|
|
|
|
fn hmac_sha256(key: &[u8], data: &[u8]) -> [u8; 32] {
|
|
type HmacSha256 = Hmac<Sha256>;
|
|
let mut hmac = HmacSha256::new_from_slice(key).expect("should construct");
|
|
hmac.update(data);
|
|
hmac.finalize_reset().into_bytes().into()
|
|
}
|
|
|
|
struct Encrypted {
|
|
iv: [u8; 16],
|
|
ciphertext: [u8; 32],
|
|
}
|
|
|
|
impl Encrypted {
|
|
fn concat(&self) -> [u8; 48] {
|
|
let mut ret = [0u8; 48];
|
|
ret[..16].copy_from_slice(&self.iv);
|
|
ret[16..].copy_from_slice(&self.ciphertext);
|
|
ret
|
|
}
|
|
}
|
|
|
|
const AUTH_BYTES: &[u8] = "auth".as_bytes();
|
|
const ENC_BYTES: &[u8] = "enc".as_bytes();
|
|
|
|
fn encrypt_hmac_sha256_siv(k: &[u8; 32], m: &[u8; 32]) -> Encrypted {
|
|
let k_a = hmac_sha256(k, AUTH_BYTES);
|
|
let k_e = hmac_sha256(k, ENC_BYTES);
|
|
let iv: [u8; 16] = hmac_sha256(&k_a, m)[..16].try_into().expect("must be 16");
|
|
let k_x = hmac_sha256(&k_e, &iv);
|
|
let c: [u8; 32] = k_x
|
|
.iter()
|
|
.zip(m.iter())
|
|
.map(|(a, b)| a ^ b)
|
|
.collect::<Vec<_>>()
|
|
.try_into()
|
|
.expect("must be length 32");
|
|
Encrypted { iv, ciphertext: c }
|
|
}
|
|
|
|
fn compare_known_hash(
|
|
pin: &[u8],
|
|
salt: [u8; 32],
|
|
master_key: [u8; 32],
|
|
expected_access_key: [u8; 32],
|
|
expected_encrypted: [u8; 48],
|
|
) {
|
|
let hashed = PinHash::create(pin, &salt).expect("should hash");
|
|
assert_eq!(hashed.access_key, expected_access_key);
|
|
|
|
let encrypted = encrypt_hmac_sha256_siv(&hashed.encryption_key, &master_key);
|
|
assert_eq!(expected_encrypted, encrypted.concat());
|
|
}
|
|
|
|
#[test]
|
|
fn known_hash() {
|
|
compare_known_hash(
|
|
b"password",
|
|
hex!("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"),
|
|
hex!("202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f"),
|
|
hex!("ab7e8499d21f80a6600b3b9ee349ac6d72c07e3359fe885a934ba7aa844429f8"),
|
|
hex!(
|
|
"3f33ce58eb25b40436592a30eae2a8fabab1899095f4e2fba6e2d0dc43b4a2d9cac5a3931748522393951e0e54dec769"
|
|
),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn known_hash2() {
|
|
compare_known_hash(
|
|
b"anotherpassword",
|
|
hex!("202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f"),
|
|
hex!("88a787415a2ecd79da0d1016a82a27c5c695c9a19b88b0aa1d35683280aa9a67"),
|
|
hex!("301d9dd1e96f20ce51083f67d3298fd37b97525de8324d5e12ed2d407d3d927b"),
|
|
hex!(
|
|
"9d9b05402ea39c17ff1c9298c8a0e86784a352aa02a74943bf8bcf07ec0f4b574a5b786ad0182c8d308d9eb06538b8c9"
|
|
),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn known_phc_string() {
|
|
let pin = b"apassword";
|
|
let phc_string = "$argon2i$v=19$m=512,t=64,p=1$ICEiIyQlJicoKSorLC0uLw$NeZzhiNv4cRmRMct9scf7d838bzmHJvrZtU/0BH0v/U";
|
|
let salt = SaltString::encode_b64(&hex!("202122232425262728292A2B2C2D2E2F")).unwrap();
|
|
|
|
let actual = local_pin_hash_with_salt(pin, &salt).unwrap();
|
|
assert_eq!(phc_string, actual);
|
|
|
|
assert!(verify_local_pin_hash(phc_string, pin).unwrap());
|
|
assert!(!verify_local_pin_hash(phc_string, b"wrongpin").unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn verify() {
|
|
let pin = b"hunter2";
|
|
let phc_string = local_pin_hash(pin).expect("should hash");
|
|
assert!(verify_local_pin_hash(&phc_string, pin).unwrap());
|
|
assert!(!verify_local_pin_hash(&phc_string, b"wrongpin").unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn known_salt() {
|
|
let username = "username";
|
|
let group_id = 3862621253427332054u64;
|
|
assert_eq!(
|
|
PinHash::make_salt(username, group_id),
|
|
hex!("d6159ba30f90b6eb6ccf1ec844427f052baaf0705da849767471744cdb3f8a5e"),
|
|
);
|
|
}
|
|
}
|