use crate::manager::models::configuration::Configuration;
use crate::storage::entities::filter::filter_entity::FilterEntity;
use crate::storage::entities::filter::filter_include_entity::FilterIncludeEntity;
use crate::storage::entities::rules_list::rules_list_entity::RulesListEntity;
use crate::{FLMError, FLMResult, FilterId};
use blake3::{derive_key as derive_key_impl, Hash, Hasher};
use std::fmt::Write;
const KEY_DERIVATION_CONTEXT: &str = "adguard-flm integrity signature v1";
pub fn generate_random_key() -> FLMResult<String> {
let mut bytes = [0u8; 32];
getrandom::fill(&mut bytes)
.map_err(|e| FLMError::Other(format!("Couldn't generate random key: {}", e)))?;
Ok(bytes.iter().fold(String::with_capacity(64), |mut acc, b| {
let _ = write!(&mut acc, "{:02x}", b);
acc
}))
}
pub(crate) fn derive_key(integrity_key: &str) -> [u8; 32] {
derive_key_impl(KEY_DERIVATION_CONTEXT, integrity_key.as_bytes())
}
pub(crate) fn derive_key_if_needed(configuration: &Configuration) -> Option<[u8; 32]> {
configuration.integrity_key.as_deref().map(derive_key)
}
pub(crate) fn sign_rules_list_entity(derived_key: &[u8; 32], entity: &mut RulesListEntity) {
entity.integrity_signature = Some(
sign(derived_key, entity.filter_id, &entity.text)
.to_hex()
.to_string(),
);
}
pub(crate) fn sign_filter_include_entity(derived_key: &[u8; 32], entity: &mut FilterIncludeEntity) {
entity.integrity_signature = Some(
sign(derived_key, entity.filter_id, &entity.body)
.to_hex()
.to_string(),
);
}
pub(crate) fn verify_rules_list_entity(
derived_key: &[u8; 32],
entity: &RulesListEntity,
) -> FLMResult<()> {
if let Some(ref key) = entity.integrity_signature {
if verify(derived_key, entity.filter_id, &entity.text, key) {
return Ok(());
}
}
Err(FLMError::FilterIntegrityCheckFailed(entity.filter_id))
}
pub(crate) fn verify_filter_include_entity(
derived_key: &[u8; 32],
entity: &FilterIncludeEntity,
) -> FLMResult<()> {
if let Some(ref key) = entity.integrity_signature {
if verify(derived_key, entity.filter_id, &entity.body, key) {
return Ok(());
}
}
Err(FLMError::FilterIntegrityCheckFailed(entity.filter_id))
}
pub(crate) fn sign_entities_if_needed(
configuration: &Configuration,
rules_entity: &mut RulesListEntity,
includes_entities: &mut [FilterIncludeEntity],
) {
if let Some(derived) = derive_key_if_needed(configuration) {
sign_rules_list_entity(&derived, rules_entity);
for include in includes_entities.iter_mut() {
sign_filter_include_entity(&derived, include);
}
}
}
pub(crate) fn sign_filter_metadata(
derived_key: &[u8; 32],
filter_id: FilterId,
download_url: &str,
subscription_url: &str,
is_trusted: bool,
is_enabled: bool,
is_installed: bool,
version: &str,
last_update_time: i64,
last_download_time: i64,
expires: i32,
) -> impl AsRef<str> + std::fmt::Display {
let mut hasher = Hasher::new_keyed(derived_key);
hasher.update(&filter_id.to_le_bytes());
let dl = download_url.as_bytes();
hasher.update(&(dl.len() as u64).to_le_bytes());
hasher.update(dl);
let sl = subscription_url.as_bytes();
hasher.update(&(sl.len() as u64).to_le_bytes());
hasher.update(sl);
hasher.update(&[is_trusted as u8]);
hasher.update(&[is_enabled as u8]);
hasher.update(&[is_installed as u8]);
let vl = version.as_bytes();
hasher.update(&(vl.len() as u64).to_le_bytes());
hasher.update(vl);
hasher.update(&last_update_time.to_le_bytes());
hasher.update(&last_download_time.to_le_bytes());
hasher.update(&expires.to_le_bytes());
hasher.finalize().to_hex()
}
pub(crate) fn sign_filter_entity(derived_key: &[u8; 32], entity: &mut FilterEntity) {
let Some(filter_id) = entity.filter_id else {
return;
};
let signature = sign_filter_metadata(
derived_key,
filter_id,
&entity.download_url,
&entity.subscription_url,
entity.is_trusted,
entity.is_enabled,
entity.is_installed,
&entity.version,
entity.last_update_time,
entity.last_download_time,
entity.expires,
);
entity.set_integrity_signature(Some(signature.to_string()));
}
pub(crate) fn sign_filter_entity_if_needed(
configuration: &Configuration,
entity: &mut FilterEntity,
) {
if let Some(derived) = derive_key_if_needed(configuration) {
sign_filter_entity(&derived, entity);
}
}
pub(crate) fn verify_filter_entity(derived_key: &[u8; 32], entity: &FilterEntity) -> bool {
let Some(filter_id) = entity.filter_id else {
return false;
};
let Some(sig) = entity.integrity_signature() else {
return false;
};
let expected = sign_filter_metadata(
derived_key,
filter_id,
&entity.download_url,
&entity.subscription_url,
entity.is_trusted,
entity.is_enabled,
entity.is_installed,
&entity.version,
entity.last_update_time,
entity.last_download_time,
entity.expires,
);
expected.as_ref() == sig
}
pub(crate) fn verify_filter_entities(
derived_key: &[u8; 32],
entities: &[FilterEntity],
) -> FLMResult<()> {
for entity in entities {
if !verify_filter_entity(derived_key, entity) {
return Err(FLMError::FilterIntegrityCheckFailed(
entity.filter_id.unwrap_or(0),
));
}
}
Ok(())
}
pub(crate) fn sign_filter_count(derived_key: &[u8; 32], count: i64) -> String {
let mut hasher = Hasher::new_keyed(derived_key);
hasher.update(b"filter_count:");
hasher.update(&count.to_le_bytes());
hasher.finalize().to_hex().to_string()
}
pub(crate) fn verify_filter_count(derived_key: &[u8; 32], count: i64, signature: &str) -> bool {
sign_filter_count(derived_key, count) == signature
}
pub(crate) fn verify_rules_list_entities(
derived_key: &[u8; 32],
entities: &[RulesListEntity],
) -> FLMResult<()> {
for entity in entities {
verify_rules_list_entity(derived_key, entity)?;
}
Ok(())
}
pub(crate) fn verify_filter_include_entities(
derived_key: &[u8; 32],
entities: &[FilterIncludeEntity],
) -> FLMResult<()> {
for entity in entities {
verify_filter_include_entity(derived_key, entity)?;
}
Ok(())
}
pub(crate) fn sign_content(derived_key: &[u8; 32], filter_id: FilterId, content: &str) -> String {
sign(derived_key, filter_id, content).to_hex().to_string()
}
pub(crate) fn verify_content(
derived_key: &[u8; 32],
filter_id: FilterId,
content: &str,
signature: &str,
) -> bool {
verify(derived_key, filter_id, content, signature)
}
fn sign(derived_key: &[u8; 32], filter_id: FilterId, content: &str) -> Hash {
let mut hasher = Hasher::new_keyed(derived_key);
hasher.update(&filter_id.to_le_bytes());
hasher.update(content.as_bytes());
hasher.finalize()
}
fn verify(derived_key: &[u8; 32], filter_id: FilterId, content: &str, signature: &str) -> bool {
let computed = sign(derived_key, filter_id, content);
computed.to_hex().as_str() == signature
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_derive_key_deterministic() {
let key1 = derive_key("my-secret-key");
let key2 = derive_key("my-secret-key");
assert_eq!(key1, key2);
}
#[test]
fn test_derive_key_different_inputs() {
let key1 = derive_key("key-a");
let key2 = derive_key("key-b");
assert_ne!(key1, key2);
}
#[test]
fn test_sign_and_verify() {
let key = derive_key("test-key");
let signature = sign(&key, 42, "some rules content");
let sig_str = signature.to_hex().to_string();
assert!(verify(&key, 42, "some rules content", &sig_str));
}
#[test]
fn test_verify_fails_on_tampered_content() {
let key = derive_key("test-key");
let signature = sign(&key, 42, "original content");
let sig_str = signature.to_hex().to_string();
assert!(!verify(&key, 42, "tampered content", &sig_str));
}
#[test]
fn test_verify_fails_on_wrong_filter_id() {
let key = derive_key("test-key");
let signature = sign(&key, 42, "content");
let sig_str = signature.to_hex().to_string();
assert!(!verify(&key, 99, "content", &sig_str));
}
#[test]
fn test_verify_fails_on_wrong_key() {
let key1 = derive_key("key-1");
let key2 = derive_key("key-2");
let signature = sign(&key1, 42, "content");
let sig_str = signature.to_hex().to_string();
assert!(!verify(&key2, 42, "content", &sig_str));
}
#[test]
fn test_sign_empty_content() {
let key = derive_key("test-key");
let signature = sign(&key, 1, "");
let sig_str = signature.to_hex().to_string();
assert!(verify(&key, 1, "", &sig_str));
assert_eq!(sig_str.len(), 64);
}
#[test]
fn test_sign_filter_metadata_and_verify_entity() {
let key = derive_key("metadata-key");
let mut entity = FilterEntity::default();
entity.filter_id = Some(42);
entity.download_url = "https://example.com/filter.txt".to_string();
entity.subscription_url = "https://example.com/subscription".to_string();
entity.is_trusted = true;
entity.is_enabled = true;
entity.is_installed = true;
entity.version = "1.2.3".to_string();
entity.last_update_time = 1700000000;
entity.last_download_time = 1700000100;
entity.expires = 86400;
sign_filter_entity(&key, &mut entity);
assert!(verify_filter_entity(&key, &entity));
entity.download_url = "https://evil.example/filter.txt".to_string();
assert!(!verify_filter_entity(&key, &entity));
}
#[test]
fn test_sign_filter_count_and_verify() {
let key = derive_key("count-key");
let sig = sign_filter_count(&key, 10);
assert!(verify_filter_count(&key, 10, &sig));
assert!(!verify_filter_count(&key, 11, &sig));
}
}