Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 45 additions & 25 deletions src/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,18 +121,8 @@
let aes_provider =
SymmetricKeyAlgorithmProvider::OpenAlgorithm(&SymmetricAlgorithmNames::AesCbcPkcs7()?)?;

// Generate IV from domain hash
let iv_data = CryptographicBuffer::ConvertStringToBinary(
&HSTRING::from(format!("IV_{}", domain)),
BinaryStringEncoding::Utf8,
)?;
let iv_hash = hash_provider.HashData(&iv_data)?;

// Take first 16 bytes of IV hash for AES-128
let mut iv_bytes: windows::core::Array<u8> = windows::core::Array::new();
CryptographicBuffer::CopyToByteArray(&iv_hash, &mut iv_bytes)?;
let iv_slice: Vec<u8> = iv_bytes.as_slice()[..16].to_vec();
let iv = CryptographicBuffer::CreateFromByteArray(&iv_slice)?;
// Generate a cryptographically random IV (16 bytes for AES-CBC)
let iv = CryptographicBuffer::GenerateRandom(16)?;

// Create symmetric key
let key = aes_provider.CreateSymmetricKey(&key_hash)?;
Expand All @@ -141,8 +131,20 @@
let data_buffer = CryptographicBuffer::CreateFromByteArray(data)?;
let encrypted_buffer = CryptographicEngine::Encrypt(&key, &data_buffer, Some(&iv))?;

// Convert to base64 string
Ok(CryptographicBuffer::EncodeToBase64String(&encrypted_buffer)?.to_string())
// Prepend IV to ciphertext so it can be extracted during decryption

Check warning on line 134 in src/windows.rs

View workflow job for this annotation

GitHub Actions / spellcheck

Unknown word (ciphertext)
let mut iv_bytes: windows::core::Array<u8> = windows::core::Array::new();
CryptographicBuffer::CopyToByteArray(&iv, &mut iv_bytes)?;
let mut encrypted_bytes: windows::core::Array<u8> = windows::core::Array::new();
CryptographicBuffer::CopyToByteArray(&encrypted_buffer, &mut encrypted_bytes)?;

let mut combined = Vec::with_capacity(iv_bytes.len() + encrypted_bytes.len());
combined.extend_from_slice(iv_bytes.as_slice());
combined.extend_from_slice(encrypted_bytes.as_slice());

let combined_buffer = CryptographicBuffer::CreateFromByteArray(&combined)?;

// Convert to base64 string (format: base64(IV || ciphertext))

Check warning on line 146 in src/windows.rs

View workflow job for this annotation

GitHub Actions / spellcheck

Unknown word (ciphertext)
Ok(CryptographicBuffer::EncodeToBase64String(&combined_buffer)?.to_string())
Comment on lines +146 to +147
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new storage format is base64(IV || ciphertext) but it does not include any version/magic marker. In decrypt_data, this makes the format ambiguous with legacy payloads (which are also base64(ciphertext)); legacy ciphertexts that are >= 2 blocks can be misinterpreted as (IV=ciphertext[0..16], ciphertext=ciphertext[16..]) and decrypt “successfully” to truncated/garbled plaintext. Consider adding an explicit prefix (e.g., "v2:" or a short magic header before the IV) so decryption can reliably select the correct format without trial-decrypting.

Copilot uses AI. Check for mistakes.
}

/// Decrypt data using Windows Hello credential
Expand Down Expand Up @@ -180,26 +182,44 @@
let aes_provider =
SymmetricKeyAlgorithmProvider::OpenAlgorithm(&SymmetricAlgorithmNames::AesCbcPkcs7()?)?;

// Generate IV from domain hash (same as encryption)
// Create symmetric key
let key = aes_provider.CreateSymmetricKey(&key_hash)?;

// Decode from base64
let raw_buffer =
CryptographicBuffer::DecodeFromBase64String(&HSTRING::from(encrypted_data))?;
let mut raw_bytes: windows::core::Array<u8> = windows::core::Array::new();
CryptographicBuffer::CopyToByteArray(&raw_buffer, &mut raw_bytes)?;

// New format: first 16 bytes are the random IV, remainder is ciphertext

Check warning on line 194 in src/windows.rs

View workflow job for this annotation

GitHub Actions / spellcheck

Unknown word (ciphertext)
if raw_bytes.len() > 16 {
let iv = CryptographicBuffer::CreateFromByteArray(&raw_bytes.as_slice()[..16])?;
let ciphertext =

Check warning on line 197 in src/windows.rs

View workflow job for this annotation

GitHub Actions / spellcheck

Unknown word (ciphertext)
CryptographicBuffer::CreateFromByteArray(&raw_bytes.as_slice()[16..])?;
Comment on lines +194 to +198
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This “try new format, then fallback” approach can return incorrect plaintext for legacy values: for a legacy ciphertext with length >= 32 bytes, treating the first 16 bytes as an IV and decrypting the remainder is valid AES-CBC input and will often pass PKCS#7 padding, yielding plaintext missing the first block. Without an unambiguous version marker (or an integrity check like a MAC/AEAD), decrypt-success is not a safe signal for format detection. Add a version/magic header (or switch to an authenticated format) and only attempt the corresponding decode/decrypt path.

Copilot uses AI. Check for mistakes.

if let Ok(decrypted_buffer) =
CryptographicEngine::Decrypt(&key, &ciphertext, Some(&iv))

Check warning on line 201 in src/windows.rs

View workflow job for this annotation

GitHub Actions / spellcheck

Unknown word (ciphertext)
{
let mut decrypted_bytes: windows::core::Array<u8> = windows::core::Array::new();
CryptographicBuffer::CopyToByteArray(&decrypted_buffer, &mut decrypted_bytes)?;
return Ok(decrypted_bytes.to_vec());
}
}

// Fallback: legacy deterministic IV for data encrypted before this fix
let iv_data = CryptographicBuffer::ConvertStringToBinary(
&HSTRING::from(format!("IV_{}", domain)),
BinaryStringEncoding::Utf8,
)?;
let iv_hash = hash_provider.HashData(&iv_data)?;

// Take first 16 bytes of IV hash for AES-128
let mut iv_bytes: windows::core::Array<u8> = windows::core::Array::new();
CryptographicBuffer::CopyToByteArray(&iv_hash, &mut iv_bytes)?;
let iv_slice: Vec<u8> = iv_bytes.as_slice()[..16].to_vec();
let iv = CryptographicBuffer::CreateFromByteArray(&iv_slice)?;
let legacy_iv_slice: Vec<u8> = iv_bytes.as_slice()[..16].to_vec();
let legacy_iv = CryptographicBuffer::CreateFromByteArray(&legacy_iv_slice)?;

// Create symmetric key
let key = aes_provider.CreateSymmetricKey(&key_hash)?;

// Decode from base64 and decrypt
let encrypted_buffer =
CryptographicBuffer::DecodeFromBase64String(&HSTRING::from(encrypted_data))?;
let decrypted_buffer = CryptographicEngine::Decrypt(&key, &encrypted_buffer, Some(&iv))?;
let decrypted_buffer =
CryptographicEngine::Decrypt(&key, &raw_buffer, Some(&legacy_iv))?;

// Convert to bytes
let mut decrypted_bytes: windows::core::Array<u8> = windows::core::Array::new();
Expand Down
Loading