use aes::Aes256;
use cipher::block_padding::NoPadding;
use cipher::{BlockDecryptMut, KeyIvInit};
use hmac::{Hmac, Mac};
use sha1::Sha1;
use sha2::Sha512;
const SALT_LEN: usize = 16;
const IV_LEN: usize = 16;
const KEY_LEN: usize = 32;
const HMAC_SALT_MASK: u8 = 0x3a;
const HMAC_KDF_ITER: u32 = 2;
const SQLITE_MAGIC: &[u8; SALT_LEN] = b"SQLite format 3\x00";
type Aes256CbcDec = cbc::Decryptor<Aes256>;
#[derive(Clone)]
pub enum SqlCipherKey {
Passphrase(Vec<u8>),
RawKey([u8; KEY_LEN]),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SqlCipherVersion {
V4,
V3,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DecryptError {
TooSmall,
KeyOrParametersMismatch,
PageAuthFailed(u32),
TooLarge,
}
pub struct Decrypted {
pub plaintext: Vec<u8>,
pub version: SqlCipherVersion,
pub page_size: u32,
}
#[derive(Clone, Copy)]
enum Prf {
Sha1,
Sha512,
}
struct Profile {
version: SqlCipherVersion,
page_size: usize,
kdf_iter: u32,
prf: Prf,
reserve: usize,
hmac_len: usize,
}
const PROFILES: [Profile; 2] = [
Profile {
version: SqlCipherVersion::V4,
page_size: 4096,
kdf_iter: 256_000,
prf: Prf::Sha512,
reserve: 80,
hmac_len: 64,
},
Profile {
version: SqlCipherVersion::V3,
page_size: 1024,
kdf_iter: 64_000,
prf: Prf::Sha1,
reserve: 48,
hmac_len: 20,
},
];
fn pbkdf2(prf: Prf, password: &[u8], salt: &[u8], rounds: u32, out: &mut [u8]) {
match prf {
Prf::Sha1 => pbkdf2::pbkdf2_hmac::<Sha1>(password, salt, rounds, out),
Prf::Sha512 => pbkdf2::pbkdf2_hmac::<Sha512>(password, salt, rounds, out),
}
}
fn hmac_ok(prf: Prf, key: &[u8], data_a: &[u8], data_b: &[u8], tag: &[u8]) -> bool {
match prf {
Prf::Sha1 => {
let Ok(mut mac) = Hmac::<Sha1>::new_from_slice(key) else {
return false; };
mac.update(data_a);
mac.update(data_b);
mac.verify_slice(tag).is_ok()
}
Prf::Sha512 => {
let Ok(mut mac) = Hmac::<Sha512>::new_from_slice(key) else {
return false; };
mac.update(data_a);
mac.update(data_b);
mac.verify_slice(tag).is_ok()
}
}
}
fn derive_keys(
profile: &Profile,
key: &SqlCipherKey,
salt: &[u8],
) -> ([u8; KEY_LEN], [u8; KEY_LEN]) {
let mut enc = [0u8; KEY_LEN];
match key {
SqlCipherKey::Passphrase(pw) => pbkdf2(profile.prf, pw, salt, profile.kdf_iter, &mut enc),
SqlCipherKey::RawKey(k) => enc.copy_from_slice(k),
}
let mut hmac_salt = [0u8; SALT_LEN];
for (dst, &s) in hmac_salt.iter_mut().zip(salt.iter()) {
*dst = s ^ HMAC_SALT_MASK;
}
let mut hmac_key = [0u8; KEY_LEN];
pbkdf2(profile.prf, &enc, &hmac_salt, HMAC_KDF_ITER, &mut hmac_key);
(enc, hmac_key)
}
struct PageLayout {
start: usize,
iv_start: usize,
}
impl PageLayout {
fn for_page(profile: &Profile, pgno: u32) -> Option<Self> {
let iv_start = profile.page_size.checked_sub(profile.reserve)?;
let start = if pgno == 1 { SALT_LEN } else { 0 };
if iv_start < start || iv_start.checked_add(IV_LEN + profile.hmac_len)? > profile.page_size
{
return None;
}
Some(Self { start, iv_start })
}
}
fn page_hmac_ok(profile: &Profile, hmac_key: &[u8], page: &[u8], pgno: u32) -> bool {
let Some(layout) = PageLayout::for_page(profile, pgno) else {
return false;
};
let (Some(auth_region), Some(tag)) = (
page.get(layout.start..layout.iv_start + IV_LEN),
page.get(layout.iv_start + IV_LEN..layout.iv_start + IV_LEN + profile.hmac_len),
) else {
return false; };
hmac_ok(profile.prf, hmac_key, auth_region, &pgno.to_le_bytes(), tag)
}
fn decrypt_page(
profile: &Profile,
enc_key: &[u8; KEY_LEN],
hmac_key: &[u8],
page: &[u8],
pgno: u32,
) -> Option<Vec<u8>> {
let layout = PageLayout::for_page(profile, pgno)?;
let iv = page.get(layout.iv_start..layout.iv_start + IV_LEN)?;
let ciphertext = page.get(layout.start..layout.iv_start)?;
let auth_region = page.get(layout.start..layout.iv_start + IV_LEN)?;
let tag = page.get(layout.iv_start + IV_LEN..layout.iv_start + IV_LEN + profile.hmac_len)?;
let tail = page.get(layout.iv_start..profile.page_size)?;
if !hmac_ok(profile.prf, hmac_key, auth_region, &pgno.to_le_bytes(), tag) {
return None;
}
if ciphertext.len() % IV_LEN != 0 {
return None; }
let dec = Aes256CbcDec::new_from_slices(enc_key, iv).ok()?;
let mut buf = ciphertext.to_vec();
let plain = dec.decrypt_padded_mut::<NoPadding>(&mut buf).ok()?;
let mut out = Vec::with_capacity(profile.page_size);
if pgno == 1 {
out.extend_from_slice(SQLITE_MAGIC);
}
out.extend_from_slice(plain);
out.extend_from_slice(tail);
Some(out)
}
fn decrypt_all(
profile: &Profile,
enc_key: &[u8; KEY_LEN],
hmac_key: &[u8],
ciphertext: &[u8],
) -> Result<Decrypted, DecryptError> {
let page_count = ciphertext.len() / profile.page_size;
let mut out = Vec::with_capacity(page_count * profile.page_size);
for i in 0..page_count {
let pgno = u32::try_from(i + 1).map_err(|_| DecryptError::TooLarge)?;
let start = i * profile.page_size;
let end = start + profile.page_size;
let page = ciphertext
.get(start..end)
.ok_or(DecryptError::PageAuthFailed(pgno))?;
let plain = decrypt_page(profile, enc_key, hmac_key, page, pgno)
.ok_or(DecryptError::PageAuthFailed(pgno))?;
out.extend_from_slice(&plain);
}
Ok(Decrypted {
plaintext: out,
version: profile.version,
page_size: u32::try_from(profile.page_size).unwrap_or(u32::MAX),
})
}
pub fn decrypt(ciphertext: &[u8], key: &SqlCipherKey) -> Result<Decrypted, DecryptError> {
if ciphertext.len() < SALT_LEN {
return Err(DecryptError::TooSmall);
}
let salt = &ciphertext[..SALT_LEN];
for profile in &PROFILES {
if ciphertext.len() < profile.page_size || ciphertext.len() % profile.page_size != 0 {
continue;
}
let (enc_key, hmac_key) = derive_keys(profile, key, salt);
let Some(page1) = ciphertext.get(..profile.page_size) else {
continue; };
if page_hmac_ok(profile, &hmac_key, page1, 1) {
return decrypt_all(profile, &enc_key, &hmac_key, ciphertext);
}
}
Err(DecryptError::KeyOrParametersMismatch)
}