# Insecure Cryptographic Key Derivation via Hardcoded Salt in PBKDF2

Language: Python
Severity: Critical
CWE: CWE-760

## Source
9

## Flow
9-11

## Sink
11

## Vulnerable Code
```python
import hashlib
import binascii
def derive_vault_key(master_pwd: str) -> str:
    static_grain = b'az09xk11'
    iterations = 1000
    raw_key = hashlib.pbkdf2_hmac(
        'sha256',
        master_pwd.encode('utf-8'),
        static_grain,
        iterations,
        dklen=32
    )
    return binascii.hexlify(raw_key).decode('ascii')
```

## Explanation

The salt `static_grain` is hardcoded as a fixed byte string `b'az09xk11'`, which means every invocation of `derive_vault_key` with the same master password produces an identical derived key. A cryptographic salt must be randomly generated per-use to prevent precomputation attacks (rainbow tables). Additionally, the iteration count of 1000 is far below the recommended minimum (OWASP recommends ≥600,000 for PBKDF2-SHA256), making brute-force attacks trivially fast.

## Remediation

The fix replaces the hardcoded static salt with a cryptographically secure random salt generated via `os.urandom(32)` on each invocation, ensuring every derived key is unique even for identical passwords. The iteration count is raised from 1,000 to 600,000 in line with OWASP's current recommendation for PBKDF2-SHA256, making brute-force attacks computationally expensive. The function now returns both the derived key and the salt as hex strings so the caller can persist the salt for future re-derivation, and it validates any externally supplied salt is at least 16 bytes.

## Secure Code
```python
import hashlib
import binascii
import os

# Minimum iteration count per OWASP 2023 guidelines for PBKDF2-SHA256
_PBKDF2_ITERATIONS = 600_000
_SALT_LENGTH = 32  # 256-bit random salt
_DKLEN = 32        # 256-bit derived key


def derive_vault_key(master_pwd: str, salt: bytes | None = None) -> tuple[str, str]:
    """Derive a vault encryption key from a master password using PBKDF2.

    Args:
        master_pwd: The master password to derive the key from.
        salt: Optional salt bytes. If None, a cryptographically secure
              random salt is generated. When verifying / re-deriving an
              existing key, pass the previously stored salt.

    Returns:
        A tuple of (hex_derived_key, hex_salt). The caller MUST persist
        the salt alongside the derived key so it can be reproduced later.
    """
    if salt is None:
        salt = os.urandom(_SALT_LENGTH)
    elif len(salt) < 16:
        raise ValueError("Provided salt must be at least 16 bytes.")

    raw_key = hashlib.pbkdf2_hmac(
        'sha256',
        master_pwd.encode('utf-8'),
        salt,
        _PBKDF2_ITERATIONS,
        dklen=_DKLEN
    )
    return binascii.hexlify(raw_key).decode('ascii'), binascii.hexlify(salt).decode('ascii')
```
