跳转至

Cryptographic Vulnerability: Insecure mode

密码学漏洞:不安全的密码模式

描述

块密码 (block cipher) 处理固定大小的数据块。通常,消息的大小大于块的大小。因此,长消息被分成一系列连续的消息块,密码算法一次对一个块进行操作。

常见的块密码模式有:

  • ECB:(Electronic Code Book,电子密码本)是处理一系列连续列出的消息块的最直接方法。用户取出第一个明文块,并使用密钥对其进行加密以生成第一个密文块。然后他取出第二个明文块,并使用相同的密钥遵循相同的过程,依此类推。这种模式是不安全的,不得使用
  • CBC:(Cipher Block Chaining,密码块链接)为生成密文提供了消息依赖性,并使系统成为非确定性的。在 CBC 模式下,当前的明文块与前一个密文块相加(XOR),然后使用密钥对结果进行加密。因此,解密是相反的过程,它涉及解密当前的密文,然后将前一个密文块添加到结果中。
  • CFB:(Cipher Feedback,密码反馈)将每个密文块“反馈”到加密过程中,以加密下一个明文块。CFB 模式需要一个初始化向量 (IV) 作为初始的随机 n 位输入块。IV 不需要保密。CFB 模式与 ECB 模式有很大不同,对应于给定明文块的密文不仅取决于该明文块和密钥,还取决于前一个密文块。换句话说,密文块依赖于消息。
  • OFB:(Output Feedback,输出反馈)涉及将底层块密码的连续输出块反馈给它自己。这些反馈块提供了为加密算法提供输入的比特流,就像 CFB 模式中的密钥流生成器一样。生成的密钥流与明文块进行 XOR 运算。OFB 模式需要一个 IV 作为初始的随机 n 位输入块。IV 不需要保密。
  • CTR:(Counter,计数器)它可以被认为是没有反馈的基于计数器的 CFB 模式版本。在这种模式下,发送方和接收方都需要访问一个可靠的计数器,该计数器在每次交换密文块时都会计算一个新的共享值。这个共享计数器不一定是保密值,但挑战在于双方必须保持计数器同步。

如果未显式指定,大多数实现将默认使用不安全的 ECB 模式。用于加密数据的操作模式存在漏洞,应将其替换为安全的模式。

建议

使用 AES 加密时,选择正确的操作模式以确保加密数据的机密性和完整性非常重要。模式的选择取决于特定的用例和安全要求。以下是一些需要考虑的常见加密模式:

  • Electronic Codebook (ECB):这是最简单的模式,涉及将明文划分为块,并使用相同的密钥分别加密每个块。但是,这种模式容易受到攻击,因为相同的明文块将产生相同的密文块。
  • Cipher Block Chaining (CBC):在这种模式下,每个明文块在加密之前都会与前一个密文块进行 XOR 运算。这增加了随机性,使得识别加密数据中的模式变得更加困难。但是,如果相同的 IV(初始化向量)被多次使用或初始化为非随机值,这种模式很容易受到攻击。
  • Counter (CTR):此模式涉及加密计数器值并将其与明文进行 XOR 运算以生成密文。这是流媒体应用程序的流行模式,因为它允许并行加密和解密。但是,它需要唯一的计数器值,如果攻击者可以猜出计数器值,它就会变得脆弱。
  • Galois/Counter Mode (GCM):此模式通过结合使用 CTR 模式和消息认证码 (MAC) 来提供机密性和完整性保护。它使用唯一的 IV 并结合了额外的附加数据(例如序列号)以防止重放攻击 (replay attacks)。这种模式常用于需要同时保护机密性和完整性的应用程序中。

通常,建议对 AES 加密使用 GCM 模式,因为它同时提供机密性和完整性保护。但是,妥善管理密钥和 IV 以确保安全至关重要。

使用认证加密模式(如 GCM)可以帮助阻止诸如 padding oracle 攻击等攻击。

import javax.crypto.Cipher
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.SecretKeySpec
import java.security.SecureRandom
import java.util.Base64

fun encrypt(plaintext: String, key: ByteArray): String {
    val cipher = Cipher.getInstance("AES/GCM/NoPadding")
    val random = SecureRandom()
    val iv = ByteArray(12)
    random.nextBytes(iv)
    val ivSpec = GCMParameterSpec(128, iv)
    val secretKey = SecretKeySpec(key, "AES")
    cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec)
    val encryptedBytes = cipher.doFinal(plaintext.toByteArray())
    val combined = ByteArray(iv.size + encryptedBytes.size)
    System.arraycopy(iv, 0, combined, 0, iv.size)
    System.arraycopy(encryptedBytes, 0, combined, iv.size, encryptedBytes.size)
    return Base64.getEncoder().encodeToString(combined)
}

fun decrypt(ciphertext: String, key: ByteArray): String {
    val cipher = Cipher.getInstance("AES/GCM/NoPadding")
    val combined = Base64.getDecoder().decode(ciphertext)
    val iv = combined.copyOfRange(0, 12)
    val ivSpec = GCMParameterSpec(128, iv)
    val secretKey = SecretKeySpec(key, "AES")
    cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec)
    val decryptedBytes = cipher.doFinal(combined.copyOfRange(12, combined.size))
    return String(decryptedBytes)
}

fun main() {
    val key = "01234567890123456789012345678901".toByteArray()
    val plaintext = "Hello, world!"
    val encryptedText = encrypt(plaintext, key)
    println("Encrypted: $encryptedText")
    val decryptedText = decrypt(encryptedText, key)
    println("Decrypted: $decryptedText")
}
import Foundation
import CryptoKit

func encrypt(plaintext: String, key: SymmetricKey) throws -> Data {
    let sealedBox = try AES.GCM.seal(plaintext.data(using: .utf8)!, using: key)
    return sealedBox.combined!
}

func decrypt(ciphertext: Data, key: SymmetricKey) throws -> String {
    let sealedBox = try AES.GCM.SealedBox(combined: ciphertext)
    let decryptedData = try AES.GCM.open(sealedBox, using: key)
    return String(data: decryptedData, encoding: .utf8)!
}

func main() {
    let key = SymmetricKey(size: .bits256)
    let plaintext = "Hello, world!"
    let encryptedData = try! encrypt(plaintext: plaintext, key: key)
    print("Encrypted: \(encryptedData.base64EncodedString())")
    let decryptedText = try! decrypt(ciphertext: encryptedData, key: key)
    print("Decrypted: \(decryptedText)")
}

main()
import 'dart:convert';
import 'dart:typed_data';
import 'package:pointycastle/export.dart';

Uint8List encrypt(Uint8List plaintext, Uint8List key) {
  final cipher = GCMBlockCipher(AESFastEngine());
  final parameters = AEADParameters(KeyParameter(key), 128, Uint8List(12));
  cipher.init(true, parameters);
  final encrypted = cipher.process(plaintext);
  final Uint8List iv = parameters.nonce;
  final combined = Uint8List(iv.length + encrypted.length);
  combined.setRange(0, iv.length, iv);
  combined.setRange(iv.length, combined.length, encrypted);
  return combined;
}

Uint8List decrypt(Uint8List ciphertext, Uint8List key) {
  final cipher = GCMBlockCipher(AESFastEngine());
  final parameters = AEADParameters(KeyParameter(key), 128, Uint8List(12));
  cipher.init(false, parameters);
  final Uint8List iv = ciphertext.sublist(0, 12);
  final encrypted = ciphertext.sublist(12);
  final decrypted = cipher.process(Uint8List.fromList(encrypted));
  return decrypted;
}

void main() {
  final key = Uint8List.fromList(List.generate(32, (index) => index)); // Your 256-bit key
  final plaintext = 'Hello, world!';
  final plaintextBytes = Uint8List.fromList(utf8.encode(plaintext));
  final encrypted = encrypt(plaintextBytes, key);
  print('Encrypted: ${base64Encode(encrypted)}');
  final decrypted = decrypt(encrypted, key);
  print('Decrypted: ${utf8.decode(decrypted)}');
}

链接

标准

  • OWASP_MASVS_L1:
    • MSTG_CRYPTO_2
    • MSTG_CRYPTO_3
    • MSTG_CRYPTO_4
  • OWASP_MASVS_L2:
    • MSTG_CRYPTO_2
    • MSTG_CRYPTO_3
    • MSTG_CRYPTO_4
  • PCI_STANDARDS:
    • REQ_2_2
    • REQ_3_6
    • REQ_3_7
    • REQ_4_2
    • REQ_6_2
  • OWASP_MASVS_v2_1:
    • MASVS_CRYPTO_1
    • MASVS_CRYPTO_2
  • SOC2_CONTROLS:
    • CC_2_1
    • CC_4_1
    • CC_6_7
    • CC_7_1
    • CC_7_2
    • CC_7_4
    • CC_7_5
  • CNIL_FOR_DEVELOPERS:
    • DEVELOPERS_4_1_4
  • HIPAA_CONTROLS:
    • SECURITY252
    • SECURITY212
    • SECURITY213