Java 隐藏主键的加密性能

Java encryption performance to hide primary keys

我需要从最终用户输出中隐藏某些原始值(主要是主键,每个请求可能大约有 100 个),这样数据库就不容易被探测或迭代。加密速度比值的实际不可渗透性更重要。

我目前在以下包装器中使用 PBEWithMD5AndDES - 这是不是太过分了?我刚开始使用 Java 作为 Web 后端。

public class SecurityHelper {
    private static final String DEFAULT_KEY = "some-key-here";
    private SecretKey secretKey;
    private PBEParameterSpec parameterSpec;
    private Cipher cipher;

    public SecurityHelper() {
        try {
            char[] moduleKeyChars = DEFAULT_KEY.toCharArray();
            KeySpec keySpec = new PBEKeySpec(moduleKeyChars);
            secretKey = SecretKeyFactory.getInstance(
              "PBEWithMD5AndDES").generateSecret(keySpec);
            parameterSpec = new PBEParameterSpec(new byte[8], 1);
            cipher = Cipher.getInstance("PBEWithMD5AndDES");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public String encrypt(String secret) {
        String encrypted = null;
        try {
            cipher.init(Cipher.ENCRYPT_MODE, secretKey, parameterSpec);
            byte[] stateBytes = cipher.doFinal(secret.getBytes("UTF-8"));
            encrypted = DatatypeConverter.printBase64Binary(stateBytes);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return encrypted;
    }

    public String decrypt(String encrypted) {
        String decrypted = null;
        try {
            cipher.init(Cipher.DECRYPT_MODE, secretKey, parameterSpec);
            byte[] stringBinary = DatatypeConverter.parseBase64Binary(encrypted);
            decrypted = new String(cipher.doFinal(stringBinary));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return decrypted;
    }

}

或者我使用 XOR cipher 这样的东西会更好吗?

如果您确实想对用户隐藏这些值,我不明白您为什么要对它们进行加密或散列处理。您应该做的是生成随机且唯一的密钥。您是否考虑过使用 GUID?

如果您认为 GUID 太长,您可以生成一个预定义长度的随机字符串。使您的数据库列成为唯一索引,以便更快地进行搜索。

这就是 URL 缩短器(例如 bit.ly 或 goo.gl 的工作原理。这将防止任何人使用主键在您的数据库中进行爬网。

是的,加密 可能有点矫枉过正。但是,您不应该陷入陷阱并因此而使用不太安全的加密原语。特别是使用 CBC 模式安全性、静态盐等确实是您应该避免的错误。 XOR或DES加密真是名不副实,分分钟能破解这些加密方案

如果加密对服务器性能影响太大,这个问题只有您可以回答。通常 IO 和复杂的 SQL 查询会给您的系统带来比几个字节数据的简单对称加密更多的任务。

我将向您展示使用 GCM 改造的 class 以及 - 如果不可用或者 12 字节的标签大小开销太大 - CTR 模式加密。

只有在无法使用(随机)GUID 时,您才应该这样做。

警告:我认为以下 classes 没有足够的输入参数检查(整数溢出等)。当我尝试实施所有这些检查和 JUnit 测试时感到无聊(无论如何都没有得到报酬 :P )。在调用任何函数之前清理您的输入。

AES/GCM:

import static java.nio.charset.StandardCharsets.*;

import java.security.GeneralSecurityException;
import java.security.SecureRandom;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;

public class SecurityHelperGCM {
    private static final int NONCE_SIZE = 8;
    private static final int TAG_SIZE = 12;
    // make sure that the hexadecimals represent a *truly random* byte array
    // (e.g. use SecureRandom)
    private final SecretKey STATIC_SECRET_KEY = new SecretKeySpec(
            hexDecode("66e517bb5fd7df840060aed7e8b58986"), "AES");
    private Cipher cipher;

    private static byte[] hexDecode(final String hex) {
        final byte[] data = new byte[hex.length() / 2];
        for (int i = 0; i < data.length; i++) {
            data[i] = (byte) Integer.parseInt(hex.substring(i * 2, i * 2 + 2),
                    16);
        }
        return data;
    }

    public SecurityHelperGCM() {
        try {
            this.cipher = Cipher.getInstance("AES/GCM/NoPadding");
        } catch (final Exception e) {
            e.printStackTrace();
        }
    }

    private static int generateRandomNonce(final byte[] nonceBuffer,
            final int offset, final int size) {
        final SecureRandom rng = new SecureRandom();
        final byte[] nonce = new byte[size];
        rng.nextBytes(nonce);
        System.arraycopy(nonce, 0, nonceBuffer, offset, size);
        clearArray(nonce);

        return offset + size;
    }

    private static void clearArray(final byte[] nonce) {
        // clean up...
        for (int i = 0; i < nonce.length; i++) {
            nonce[i] = 0;
        }
    }

    private static GCMParameterSpec generateGCMParametersFromNonce(
            final byte[] nonceBuffer, final int offset, final int size,
            final int blockSize) {
        final GCMParameterSpec gcmParameters = new GCMParameterSpec(TAG_SIZE
                * Byte.SIZE, nonceBuffer, offset, size);
        return gcmParameters;
    }

    public String encrypt(final String secret) {
        final byte[] plaintext = secret.getBytes(UTF_8);
        final byte[] nonceAndCiphertext = new byte[NONCE_SIZE
                + plaintext.length + TAG_SIZE];

        int offset = generateRandomNonce(nonceAndCiphertext, 0, NONCE_SIZE);
        final GCMParameterSpec nonceIV = generateGCMParametersFromNonce(
                nonceAndCiphertext, 0, NONCE_SIZE, this.cipher.getBlockSize());

        try {
            this.cipher.init(Cipher.ENCRYPT_MODE, this.STATIC_SECRET_KEY,
                    nonceIV);
            offset += this.cipher.doFinal(plaintext, 0, plaintext.length,
                    nonceAndCiphertext, offset);
            if (offset != nonceAndCiphertext.length) {
                throw new IllegalStateException(
                        "Something wrong during encryption");
            }
            // Java 8 contains java.util.Base64
            return DatatypeConverter.printBase64Binary(nonceAndCiphertext);
        } catch (final GeneralSecurityException e) {
            throw new IllegalStateException(
                    "Missing basic functionality from Java runtime", e);
        }
    }

    public String decrypt(final String encrypted) throws BadPaddingException {
        final byte[] nonceAndCiphertext = DatatypeConverter
                .parseBase64Binary(encrypted);
        final GCMParameterSpec nonceIV = generateGCMParametersFromNonce(
                nonceAndCiphertext, 0, NONCE_SIZE, this.cipher.getBlockSize());
        try {
            this.cipher.init(Cipher.DECRYPT_MODE, this.STATIC_SECRET_KEY,
                    nonceIV);
            final byte[] plaintext = this.cipher.doFinal(nonceAndCiphertext,
                    NONCE_SIZE, nonceAndCiphertext.length - NONCE_SIZE);
            return new String(plaintext, UTF_8);
        } catch (final BadPaddingException e) {
            throw e;
        } catch (final GeneralSecurityException e) {
            throw new IllegalStateException(
                    "Missing basic functionality from Java runtime", e);
        }
    }

    public static void main(final String[] args) {
        final String secret = "owlstead";
        final SecurityHelperGCM securityHelperGCM = new SecurityHelperGCM();
        final String ct = securityHelperGCM.encrypt(secret);
        String pt = null;
        try {
            pt = securityHelperGCM.decrypt(ct);
        } catch (BadPaddingException e) {
            System.out.println("Ciphertext tampered, take action!");
        }
        System.out.println(pt);
    }
}

AES/CTR:

import static java.nio.charset.StandardCharsets.*;

import java.security.GeneralSecurityException;
import java.security.SecureRandom;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;

public class SecurityHelperCTR {
    private static final int NONCE_SIZE = 8;
    // make sure that the hexadecimals represent a *truly random* byte array
    // (e.g. use SecureRandom)
    private final SecretKey STATIC_SECRET_KEY = new SecretKeySpec(
            hexDecode("66e517bb5fd7df840060aed7e8b58986"), "AES");
    private Cipher cipher;

    private static byte[] hexDecode(final String hex) {
        final byte[] data = new byte[hex.length() / 2];
        for (int i = 0; i < data.length; i++) {
            data[i] = (byte) Integer.parseInt(hex.substring(i * 2, i * 2 + 2),
                    16);
        }
        return data;
    }

    public SecurityHelperCTR() {
        try {
            this.cipher = Cipher.getInstance("AES/CTR/NoPadding");
        } catch (final Exception e) {
            e.printStackTrace();
        }
    }

    private static int generateRandomNonce(final byte[] nonceBuffer,
            final int offset, final int size) {
        final SecureRandom rng = new SecureRandom();
        final byte[] nonce = new byte[size];
        rng.nextBytes(nonce);
        System.arraycopy(nonce, 0, nonceBuffer, offset, size);
        return offset + size;
    }

    private static IvParameterSpec generateIVFromNonce(
            final byte[] nonceBuffer, final int offset, final int size,
            final int blockSize) {
        final byte[] ivData = new byte[blockSize];
        System.arraycopy(nonceBuffer, offset, ivData, 0, size);
        final IvParameterSpec iv = new IvParameterSpec(ivData);
        return iv;
    }

    public String encrypt(final String secret) {
        final byte[] plaintext = secret.getBytes(UTF_8);
        final byte[] nonceAndCiphertext = new byte[NONCE_SIZE
                + plaintext.length];

        int offset = generateRandomNonce(nonceAndCiphertext, 0, NONCE_SIZE);
        final IvParameterSpec nonceIV = generateIVFromNonce(nonceAndCiphertext,
                0, NONCE_SIZE, this.cipher.getBlockSize());

        try {
            this.cipher.init(Cipher.ENCRYPT_MODE, this.STATIC_SECRET_KEY,
                    nonceIV);
            offset += this.cipher.doFinal(plaintext, 0, plaintext.length,
                    nonceAndCiphertext, offset);
            if (offset != nonceAndCiphertext.length) {
                throw new IllegalStateException(
                        "Something wrong during encryption");
            }
            // Java 8 contains java.util.Base64
            return DatatypeConverter.printBase64Binary(nonceAndCiphertext);
        } catch (final GeneralSecurityException e) {
            throw new IllegalStateException(
                    "Missing basic functionality from Java runtime", e);
        }
    }

    public String decrypt(final String encrypted) {
        final byte[] nonceAndCiphertext = DatatypeConverter
                .parseBase64Binary(encrypted);
        final IvParameterSpec nonceIV = generateIVFromNonce(nonceAndCiphertext,
                0, NONCE_SIZE, this.cipher.getBlockSize());
        try {
            this.cipher.init(Cipher.DECRYPT_MODE, this.STATIC_SECRET_KEY,
                    nonceIV);
            final byte[] plaintext = this.cipher.doFinal(nonceAndCiphertext,
                    NONCE_SIZE, nonceAndCiphertext.length - NONCE_SIZE);
            // note: this may return an invalid result if the value is tampered
            // with
            // it may even contain more or less characters
            return new String(plaintext, UTF_8);
        } catch (final GeneralSecurityException e) {
            throw new IllegalStateException(
                    "Missing basic functionality from Java runtime", e);
        }
    }

    public static void main(final String[] args) {
        final String secret = "owlstead";
        final SecurityHelperCTR securityHelper = new SecurityHelperCTR();
        final String ct = securityHelper.encrypt(secret);
        final String pt = securityHelper.decrypt(ct);
        System.out.println(pt);
    }
}