Skip to content

fix(windows): use cryptographically random IV for AES-CBC encryption#46

Open
kofany wants to merge 1 commit intoChoochmeque:mainfrom
kofany:fix/windows-randomize-aes-cbc-iv
Open

fix(windows): use cryptographically random IV for AES-CBC encryption#46
kofany wants to merge 1 commit intoChoochmeque:mainfrom
kofany:fix/windows-randomize-aes-cbc-iv

Conversation

@kofany
Copy link

@kofany kofany commented Feb 7, 2026

Summary

The current Windows implementation derives the AES-CBC IV deterministically from the domain name (SHA-256("IV_" + domain)). This violates AES-CBC security requirements — IVs must be unpredictable and unique per encryption operation. Reusing the same IV for a given domain enables pattern analysis when data is updated (an attacker with access to the vault can detect whether the stored value has changed).

Changes:

  • Replace deterministic IV with CryptographicBuffer::GenerateRandom(16) (CSPRNG)
  • Store as base64(IV || ciphertext) — IV is prepended to ciphertext before encoding
  • Backward compatible: decryption first tries the new format (extract IV from first 16 bytes), then falls back to the legacy deterministic IV for previously encrypted data

No changes to macOS/iOS/Android implementations (they correctly delegate encryption to OS-level Keychain/KeyStore).

References

The previous implementation derived the IV deterministically from the
domain name using SHA-256("IV_" + domain). This violates AES-CBC security
requirements: IVs must be unpredictable and unique per encryption
operation. Reusing the same IV for the same domain allows pattern
analysis across ciphertext updates.

Replace the deterministic IV with CryptographicBuffer::GenerateRandom(16)
and prepend the IV to the ciphertext before base64 encoding. The stored
format becomes base64(IV || ciphertext).

Decryption detects the format automatically: it first attempts to extract
the IV from the first 16 bytes (new format), and falls back to the legacy
deterministic IV for data encrypted before this change.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the Windows-specific encryption format used when storing data in the Windows PasswordVault, fixing an insecure deterministic AES-CBC IV by switching to a per-encryption cryptographically random IV while attempting to remain backward compatible for existing stored values.

Changes:

  • Replace deterministic IV derivation (SHA-256("IV_" + domain)) with a random 16-byte IV.
  • Store encrypted payload as base64(IV || ciphertext) by prepending the IV to the ciphertext.
  • Update decryption to try the new format first, then fall back to the legacy deterministic-IV scheme.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +146 to +147
// Convert to base64 string (format: base64(IV || ciphertext))
Ok(CryptographicBuffer::EncodeToBase64String(&combined_buffer)?.to_string())
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.
Comment on lines +194 to +198
// New format: first 16 bytes are the random IV, remainder is ciphertext
if raw_bytes.len() > 16 {
let iv = CryptographicBuffer::CreateFromByteArray(&raw_bytes.as_slice()[..16])?;
let ciphertext =
CryptographicBuffer::CreateFromByteArray(&raw_bytes.as_slice()[16..])?;
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants