Back to Blog
By AriesZhou · · 8 min read

Cryptography Lessons: Common Pitfalls and Best Practices

Cryptography

Quoted from stackexchange

Encryption is the first line of defense for information security, but it is also where mistakes are most easily made. Even experienced engineers can stumble in applied cryptography. A single key reuse, an improper IV, or choosing the wrong encryption mode can lead to the entire system being compromised. These lessons come from real vulnerabilities and deserve a closer look.

Don’t invent algorithms

Don’t invent your own encryption algorithms or protocols, because mistakes are easy to make. Cryptographic algorithms are complex and require extensive, rigorous review to ensure their security. Self-invented algorithms or protocols usually cannot provide sound mathematical reasoning to prove their security.

The best choice is to use standard cryptographic algorithms and protocols. A standard algorithm is usually born to solve certain past problems, and those problems are very likely the ones you are facing. For example:

  • Use TLS or SSL to solve communication security
  • Use GPG or PGP to solve data-at-rest security

These are well-vetted cryptographic schemes. If you need to use cryptographic interfaces to implement certain functionality, prefer high-level crypto libraries such as cryptlib, GPGME, Keyczar, etc., rather than low-level libraries like OpenSSL, CryptoAPI, JCE, which are error-prone and difficult to apply correctly.

Don’t use unauthenticated encryption

This is a very common mistake: encrypting data without authenticating it.

For example, a developer wants to protect sensitive information and encrypts the message using AES-CBC mode.

This only achieves message confidentiality, but cannot guarantee security against active attacks such as tampering, replay attacks, and reflection attacks.

Adding message authentication solves this problem.

Many online systems have suffered serious vulnerabilities due to this issue, such as ASP. NET, XML encryption, Amazon EC2, JavaServer Faces, Ruby on Rails, OWASP ESAPI, IPSEC, WEP, ASP. NET again, and SSH2.

To avoid this problem, message authentication should be used with every encryption. There are two mainstream approaches:

  • The simplest way is to use an off-the-shelf authenticated encryption scheme such as GCM, CWC, EAX, CCM, or OCB. These methods encrypt and authenticate data simultaneously, so the user no longer needs to worry about this issue.
  • Another approach is to implement message authentication yourself. First, encrypt the message with an appropriate symmetric encryption method (e.g., AES-CBC), then generate a message authentication code (such as AES-CMAC, SHA1-HMAC, or SHA256-HMAC) over the ciphertext, and append the generated MAC digest to the ciphertext before transmitting the data. On the receiving end, verify the validity of the MAC digest before decrypting the data.
graph LR
    A[明文] -->|加密| B[AES-CBC]
    B --> C[密文]
    C -->|计算 MAC| D[HMAC]
    D --> E[密文 + MAC]

    F[接收端] -->|验证 MAC| G[HMAC 验证]
    G -->|通过| H[解密]
    G -->|失败| I[丢弃数据]

GCM mode is recommended because it automatically provides authentication during encryption, avoiding the complexity of manually implementing a MAC.

Do not use the same key for encryption, authentication, or signing

A key should not be reused for multiple purposes; doing so expands the risk.

For example, if you have an RSA key pair, you should not use it for both encryption and signing. If you need both encryption and signing, generate two key pairs.

Similarly, for symmetric encryption, you should not use the encryption key to generate message authentication codes. In short, do not reuse keys for different purposes.

Be careful when hashing concatenated strings

For example, a developer wants to obtain a hash of strings S and T. They concatenate S and T and hash the result, obtaining H(S||T). This is flawed.

The problem is that concatenation makes the boundary between the two strings ambiguous. For example, builtin||securely = built||insecurely, the hash operation cannot distinguish string S from string T. As a result, an attacker may be able to change the boundary between the two strings without changing the hash value. For example, if Alice wants to send two strings builtin and securely, an attacker could change the strings to built and insecurely without invalidating the hash value.

Similar problems arise when performing digital signatures or message authentication code operations on concatenated strings.

The solution is not to concatenate plaintext directly, but to use an encoding that can be decoded unambiguously. For example, when you want to compute H(S||T), consider using H(length(S)||S||T) instead, where length(S) is a 32-bit value representing the length of the string S; alternatively, use approaches such as H(H(S)||H(T)) or H(H(S)||T).

For related cases on this issue, see this flaw in Amazon Web Services, this flaw in Flickr.

Do not reuse nonces or IVs

Many encryption modes require an IV (initialization vector). Never reuse the same IV, as this can lead to serious vulnerabilities.

graph LR
    subgraph 错误示例: IV 重用
        A1[明文 A] -->|IV=123| B1[加密]
        A2[明文 B] -->|IV=123| B2[加密]
        B1 --> C1[密文 A]
        B2 --> C2[密文 B]
        C1 -.-> D[攻击者 XOR]
        C2 -.-> D
        D --> E[获得明文 B]
    end
  • For stream ciphers, such as CTR or OFB mode, this can allow encrypted data to be easily recovered as plaintext
  • For other modes such as CBC, reusing an IV can also lead to plaintext recovery attacks

Therefore, regardless of which encryption mode you use, you should not reuse an IV.

Generate a new random IV for every encryption, and do the same for the nonce in GCM mode.

Ensure the random number generator has sufficient entropy

Make sure to use a cryptographically secure pseudorandom number generator for producing keys, IVs, nonces, and so on, rather than using rand(), random(), drand48() and the like.

Ensure the random number generator is seeded with enough entropy. Do not use dates as seeds, since they are predictable.

For example: srand(time(NULL)) is not good. A better approach is to seed the random number generator with 128 bits of data or true random numbers, for example through /dev/urandom, CryptGenRandom or similar tools. In Java, use SecureRandom instead of Random. In . NET, use System. Security. Cryptography. RandomNumberGenerator instead of System. Random. In Python, use random. SystemRandom instead of random.

For symmetric encryption, do not use ECB mode

For example, the plaintext of an image is shown below:

明文

The result after encrypting this data with ECB mode:

The result after encrypting this data with CBC mode:

Avoid using passwords as keys

A common mistake is to use a password or its hash as the encryption key. Keys need sufficient entropy, which most passwords and passphrases lack, making them vulnerable to dictionary attacks.

If you must derive a key from a password, you can increase the difficulty of dictionary attacks through iterative hashing:

// ❌ 错误:直接用密码哈希作为密钥
const badKey = sha256(password);

// ✅ 正确:使用 PBKDF2 硬化
const crypto = require('crypto');
const key = crypto.pbkdf2Sync(password, salt, 100000, 32, 'sha256');

// ✅ 正确:使用 bcrypt(更推荐)
const bcrypt = require('bcrypt');
const hashedKey = await bcrypt.hash(password, 12);  // 12 是工作因子

It is recommended to use Argon2id (a winner of the Password Hashing Competition) or bcrypt for password hardening, as these algorithms offer better defense against hardware-accelerated attacks.

Do not use insecure key lengths

Ensure you use sufficiently long keys. For example, 1024-bit RSA keys were proven insecure around 2010; it is recommended to use RSA keys of at least 2048 bits.

For symmetric ciphers, keys of 128 bits or more should be used.


Summary

PrincipleRiskBest Practice
Don’t invent algorithmsSecurity cannot be provenUse standard algorithms (TLS, GPG, AES)
Use authenticated encryptionData can be tampered withUse authenticated encryption modes like GCM, EAX
Key separationHigh risk from a single key compromiseUse separate keys for encryption, signing, and authentication
Concatenate hashes correctlyAmbiguous boundaries can be exploitedUse length prefixes or nested hashing
Don’t reuse IV/NoncePlaintext can be recoveredGenerate a random IV for each encryption
Use secure random numbersKeys can be predictedUse a CSPRNG (e.g., /dev/urandom)
Avoid ECB modeMode reveals plaintext structureUse modes like CBC, GCM
Avoid using passwords directly as keysInsufficient entropy makes them easy to crackUse PBKDF2, bcrypt for hardening
Use sufficient key lengthsRisk of brute-force attacksRSA ≥2048-bit, symmetric ≥128-bit

The pitfalls of cryptography often hide in the details. Using mature libraries, following best practices, and understanding common vulnerabilities are key to protecting system security.