Hardcoded Cryptographic Salt

ID

swift.hardcoded_cryptographic_salt

Severity

high

Resource

Predictability

Language

Swift

Tags

CWE:760, MASWE:0013, NIST.SP.800-53, OWASP:2021:A2, PCI-DSS:6.5.3

Description

"Salting" is a technique used in some cryptographic operations:

  • With password hashing, a salt is typically used to prevent dictionary or 'rainbow' table attacks using massive pre-computed tables of hashed common passwords.

  • With key derivation functions (KDF), such as PBKDF2, scrypt or Argon2, a salt is used to prevent the same key being derived from the same input password.

  • With digital signatures and message authentication schemes, salts often add randomness to the message being signed or authenticated, making it harder for attackers to forge signatures or extract private keys through cryptoanalysis.

The use of a hardcoded cryptographic salt in applications can severely compromise the security of cryptographic processes. In password hashing, for example, a salt is typically used to ensure that even if two users have the same password, their hashed outputs will differ.

However, if the salt is hardcoded into the application’s source code, it becomes predictable and defeats the purpose of introducing randomness, making the application susceptible to attacks such as dictionary and rainbow table attacks.

Rationale

Hardcoding cryptographic salts within the source code is a common error that can reduce the effectiveness of hash functions used to secure sensitive information, like passwords.

A salt should be unique and random for each cryptographic operation to ensure that the results are distinct, even for identical inputs. By hardcoding a salt, attackers can reverse-engineer the application to discover the salt, allowing them to perform attacks that target the hashed data.

Consider the following vulnerable Swift example using CryptoSwift:

import CryptoSwift

class PasswordHasher {
    // FLAW: Hardcoded salt
    private static let constantSalt: [UInt8] = [0x2a, 0x3a, 0x80, 0x05, 0xaf, 0x80, 0x05, 0xaf]

    func hashPassword(_ password: String) throws -> String {
        // FLAW: Using constant salt for PBKDF2
        let pbkdf2 = try PBKDF2(
            password: Array(password.utf8),
            salt: PasswordHasher.constantSalt, // FLAW
            iterations: 10000,
            keyLength: 32,
            variant: .sha256
        )

        return try pbkdf2.calculate().toHexString()
    }
}

Another vulnerable example with CommonCrypto:

import Foundation
import CommonCrypto

func deriveKeyFromPassword_Vulnerable(_ password: String) -> Data? {
    // FLAW: Hardcoded salt
    let salt = "ConstantSaltValue123".data(using: .utf8)!  // FLAW

    var derivedKey = Data(count: 32)
    let status = derivedKey.withUnsafeMutableBytes { derivedKeyBytes in
        password.data(using: .utf8)!.withUnsafeBytes { passwordBytes in
            salt.withUnsafeBytes { saltBytes in
                CCKeyDerivationPBKDF(
                    CCPBKDFAlgorithm(kCCPBKDF2),
                    passwordBytes.baseAddress?.assumingMemoryBound(to: Int8.self),
                    password.count,
                    // predicatable salt
                    saltBytes.baseAddress?.assumingMemoryBound(to: UInt8.self),
                    salt.count,
                    CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA256),
                    10000,
                    derivedKeyBytes.baseAddress?.assumingMemoryBound(to: UInt8.self),
                    32
                )
            }
        }
    }

    guard status == kCCSuccess else { return nil }
    return derivedKey
}

In these examples, the same salt is used for all password hashing operations. An attacker who obtains these hashes can build a single rainbow table and crack multiple passwords efficiently.

Remediation

To remediate the issue of a hardcoded cryptographic salt, developers should generate a unique, random salt for each cryptographic operation. This ensures the robustness of the hash function by making the output unique even for identical inputs.

Generate a unique, non-predictable salt for each cryptographic operation. Store the generated salt in clear text along with the hashed/encrypted data to allow so the salt could be retrieved during the verification process. And never reuse a salt !

In the 1970s 12 bits of salt were common and appropriate for the computational and storage capacity at that time. Today, at least 128 bits of salt are recommended and generally are robust enough for most use cases.

Secure implementation using CryptoSwift with random salt:

import CryptoSwift
import Foundation

class SecurePasswordHasher {

    struct HashedPassword {
        let hash: Data
        let salt: Data
    }

    func hashPassword(_ password: String) throws -> HashedPassword {
        // FIXED: Generate random salt for each password
        let salt = generateRandomSalt(length: 16)

        let pbkdf2 = try PBKDF2(
            password: Array(password.utf8),
            salt: salt,
            iterations: 100000,  // Increased iterations for better security
            keyLength: 32,
            variant: .sha256
        )

        let hash = try pbkdf2.calculate()

        return HashedPassword(hash: Data(hash), salt: Data(salt))
    }

    private func generateRandomSalt(length: Int) -> [UInt8] {
        return (0..<length).map { _ in UInt8.random(in: 0...UInt8.max) }
    }

    func verifyPassword(_ password: String, against stored: HashedPassword) throws -> Bool {
        // Use the stored salt for verification
        let pbkdf2 = try PBKDF2(
            password: Array(password.utf8),
            salt: Array(stored.salt),
            iterations: 100000,
            keyLength: 32,
            variant: .sha256
        )

        let computedHash = try pbkdf2.calculate()
        return Data(computedHash) == stored.hash
    }
}

Secure implementation using CommonCrypto with random salt:

import Foundation
import CommonCrypto

struct PasswordHash {
    let hash: Data
    let salt: Data
    let iterations: Int
}

func secureHashPassword(_ password: String, iterations: Int = 100000) throws -> PasswordHash {
    // FIXED: Generate cryptographically secure random salt
    var salt = Data(count: 16)
    let result = salt.withUnsafeMutableBytes { saltBytes in
        SecRandomCopyBytes(kSecRandomDefault, 16, saltBytes.baseAddress!)
    }

    guard result == errSecSuccess else {
        throw NSError(domain: "RandomGenerationError", code: Int(result), userInfo: nil)
    }

    // Derive key using random salt
    var derivedKey = Data(count: 32)
    let status = derivedKey.withUnsafeMutableBytes { derivedKeyBytes in
        password.data(using: .utf8)!.withUnsafeBytes { passwordBytes in
            salt.withUnsafeBytes { saltBytes in
                CCKeyDerivationPBKDF(
                    CCPBKDFAlgorithm(kCCPBKDF2),
                    passwordBytes.baseAddress?.assumingMemoryBound(to: Int8.self),
                    password.count,
                    saltBytes.baseAddress?.assumingMemoryBound(to: UInt8.self),
                    salt.count,
                    CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA256),
                    UInt32(iterations),
                    derivedKeyBytes.baseAddress?.assumingMemoryBound(to: UInt8.self),
                    32
                )
            }
        }
    }

    guard status == kCCSuccess else {
        throw NSError(domain: "PBKDFError", code: Int(status), userInfo: nil)
    }

    return PasswordHash(hash: derivedKey, salt: salt, iterations: iterations)
}