大文本数据的非对称密码算法

Asymmetric cryptographic algorithm for large text data

我正在寻找满足以下要求的解决方案:

让我们假设,有:安装在计算设备上并控制它的应用程序,使用该应用程序的用户,以及为该应用程序提供一些支持的维护者。应用程序具有配置,例如在文件或数据库中。配置由维护人员在需要时手动更新,例如每周一次。配置包含,例如,电子邮件列表,应用程序将其警报发送到。让我们假设,用户不可能以任何方式修改应用程序。虽然,Application 是用Java 编写的,所以用户很容易复制和调试它。在内部,应用程序解密应用程序内存中的配置,以便使用配置。

用户应能够从应用程序内部查看配置。用户不得更改配置或使用自己的配置(基本相同),例如更改任何电子邮件或删除现有电子邮件或添加新电子邮件。

附加要求,非强制性要求:没有应用程序将无法直接查看配置。我知道这几乎不可能,所以,至少应该是困难的,比如在没有应用程序的情况下查看配置所需的解密。

问题:如何实现,是否可能?

我能实现的可能解决方案和攻击:

1) 使用一些签名。使用一些摘要对每个配置进行签名,然后在应用程序中检查摘要。攻击:据我了解,应用程序应使用存储在其中的 public 密钥计算摘要。然后,应用程序应将计算出的摘要与随配置提供的摘要进行比较。所以,攻击很简单:用户修改Configuration,然后调试Application,在Application已经计算出Digest的地方下断点,与存储的Digest进行比较,然后用户可以dump计算出的Digest,并用这个替换提供的Digest计算出一个。

2) 使用混合加密。在这种情况下,攻击是相同的:在解密的对称密钥可用的地方断点,转储此密钥,然后将其用于新的配置加密。

3) 使用非对称加密。维护者使用 public 密钥加密配置,然后应用程序使用私钥解密配置。攻击很简单:用户可以从应用程序中转储私钥并派生一个新的 public 密钥,然后将其用于加密。

是否有针对大块数据(最多 10 kb)的解决方案,例如 "encrypt with the public key, then decrypt with private",或者任何其他可能的解决方案?

谢谢

各位同事, 我有一个应接收和存储一些只读数据的应用程序。数据应可供用户阅读但不可更改。例如,假设存在包含数据的文本文件,这些文件保留了一些可供最终用户阅读但不可编辑的文本。应用程序应定期接收新文件,并应以相同的只读模式供用户使用。 因此,用户可以访问本地内容(例如文件内容)。用户可以从应用程序复制内容、保存解密副本等。我唯一需要防止的是用任何其他数据替换现有数据,包括更改内容或由用户添加新数据。我不需要更改检测,我需要使更改变得不可能(好吧,越难越好)。 我想,最简单的方法是用一些密钥加密数据并在应用程序中包含 public 密钥,这样应用程序就可以解密和显示数据,但如果没有密钥,用户就不会能够更改内容。 我知道,标准 RSA 只支持小数据块加密,通常略小于密钥长度。 (我进行了测试,发现对于 RSA 2048 Java 在 254 字节后抛出异常)我还读到,将源数据拆分为块然后加密这些块并不是一个好主意。我读到建议使用对称密钥(如 AES)进行加密和解密,然后使用 RSA 密钥对加密此 AES 密钥。 在这种情况下,我看到了一个很大的(我猜想的)安全风险——因为我的应用程序是用 Java 编写的,调试它并转储解密的 AES 密钥,然后使用它进行数据修改非常容易,甚至无需对应用程序本身进行任何修改。 所以,我的问题是:如何解决这个问题以及在这种情况下使用什么被认为是安全的? 谢谢 更新: 当然,用户可以复制文件并根据需要使用副本。目标是禁止用户更改应用程序使用的数据,而不是这些数据的副本。在非对称加密的情况下,它很容易实现——我用我的私钥加密数据,传递给应用程序,应用程序在运行时用它的 public 密钥解密数据并使用。如果有人想更改数据,应用程序将无法正确解密数据,数据将被破坏,应用程序将无法运行,直到数据被还原。

首先,don't roll your own crypto. Cryptography is very hard, and if you make any mistake, it will have vulnerabilities you could have avoided by using a well-established library to do the heavy lifting. You could, for example, use libsodium。它有很多抽象,并且可能有满足您需要的解决方案。

说完这个,让我们讨论一下如何让它更安全:用户需要能够阅读内容,但不能编辑它。 "cannot edit" 到底是什么意思?难道他不能在本地修改任何东西,或者只是不能像他被授权那样上传到你的服务器吗?

如果是前者,加密对你帮助不大——你需要能够在本地解密它,这样攻击者就可以随时转储你进程的内存来获取数据——当然这很难,但是绝对有可能。只是不允许人们 edit/save/download 在您的应用程序中将是您可以获得的最有力保证。

如果是后者,那么使用身份验证将是可行的方法 - 可以是一种简单的方法,例如使用用户和密码进行 HTTP 基本身份验证,或者对要上传的文件进行签名。在您的应用程序端处理身份验证将是更实用的方法。

一个有趣的问题。据我从你的问题中可以看出,数据本身并不是秘密。您的问题是用户不应该能够更改数据(或者您应该能够检测到他或她已经更改了数据)。在这种情况下,哈希函数(可能包括加密哈希函数)可能是更好的方法。参见 https://en.wikipedia.org/wiki/Hash_function and https://en.wikipedia.org/wiki/Cryptographic_hash_function。如果你使用散列函数,那么你总是可以检测到用户是否试图改变数据。

加密散列函数是单向的,因此您不需要在程序中存储任何密钥。

非对称加密是使用Public密钥完成的,解密是使用私钥执行的。由于应用程序必须能够解密数据,应用程序需要知道私钥,这就是常用算法的问题,因为对于 RSA 或 ECIES,Public 密钥可以从私钥派生。因此,在使用 Public 密钥加密后派生 Public 密钥并存储 changed/appended 数据并不是真正的问题。

第二件事是 - 您没有指定 "large" 您的文本将如何 - 一些 KB、MB、GB?

几个月前,我测试了一些 "new" 算法,它们是 "Post quantum safe",作为示例,我使用了 Bouncy Castle Crypto 提供程序提供的 McEliece Fujisaki 算法(我使用版本 1.65,bcprov -jdk15to18-165.jar).

该程序创建一个 50 MB 的大字节数组,该数组使用 Public 密钥加密并使用私钥解密。 目前我没有找到任何 Public 密钥派生方法,因此您肯定需要知道 Private 和 Public 密钥。 我没有测试更大的字节数组,因为这个参数取决于目标系统的内存(你需要双内存 因为完整的数据在 ciphertextByte 中捕获,然后在 decryptedtextByte 中再次捕获)。

编辑 2020 年 6 月 16 日: 总统 James K. Polk 编写了一种方法,可以轻松地从给定的私钥中检索 public 密钥。来源可在他的 GitHub-Repo (https://github.com/james-k-polk/McEliece/blob/master/McElieceRecoverPublicFromPrivate.java) 中找到,为方便起见,在此答案末尾显示。 所以每个有权访问私人 McEliece 密钥的人都能够使用检索到的 public 密钥加密数据! 感谢 James 总统的帮助。

以下是控制台上的输出:

McEliece Fujisaki Pqc Encryption
key generation
PrivateKey length: 4268   algorithm: McEliece-CCA2 format: PKCS#8
PublicKey  length: 103429 algorithm: McEliece-CCA2 format: X.509

initialize cipher for encryption
pt length:    52428800 (50 mb)
ct length:    52429056 (50 mb)

initialize cipher for decryption
dt length:    52428800 (50 mb)

compare plaintext <-> decryptedtext: true

class McElieceFujisakiPqcEncryptionLargeData.java

import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
import org.bouncycastle.crypto.params.ParametersWithRandom;
import org.bouncycastle.pqc.crypto.mceliece.*;
import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider;
import org.bouncycastle.pqc.jcajce.provider.mceliece.BCMcElieceCCA2PrivateKey;
import org.bouncycastle.pqc.jcajce.provider.mceliece.BCMcElieceCCA2PublicKey;

import java.security.*;
import java.util.Arrays;
import java.util.Random;

public class McElieceFujisakiPqcEncryptionLargeData {
    public static void main(String[] args) throws InvalidCipherTextException {
        System.out.println("McEliece Fujisaki Pqc Encryption");
        if (Security.getProvider("BCPQC") == null) {
            Security.addProvider(new BouncyCastlePQCProvider());
            // used Bouncy Castle: bcprov-jdk15to18-165.jar
        }
        System.out.println("key generation");
        SecureRandom keyRandom = new SecureRandom();
        McElieceCCA2Parameters params = new McElieceCCA2Parameters();
        McElieceCCA2KeyPairGenerator mcElieceCCA2KeyGen = new McElieceCCA2KeyPairGenerator();
        McElieceCCA2KeyGenerationParameters genParam = new McElieceCCA2KeyGenerationParameters(keyRandom, params);
        mcElieceCCA2KeyGen.init(genParam);
        AsymmetricCipherKeyPair pair = mcElieceCCA2KeyGen.generateKeyPair();
        AsymmetricKeyParameter mcEliecePrivateKey = pair.getPrivate();
        AsymmetricKeyParameter mcEliecePublicKey = pair.getPublic();
        PrivateKey privateKey = new BCMcElieceCCA2PrivateKey((McElieceCCA2PrivateKeyParameters) pair.getPrivate()); // conversion neccessary only for key data
        PublicKey publicKey = new BCMcElieceCCA2PublicKey((McElieceCCA2PublicKeyParameters) pair.getPublic()); // conversion neccessary only for key data
        System.out.println("PrivateKey length: " + privateKey.getEncoded().length + "   algorithm: " + privateKey.getAlgorithm() + " format: " + privateKey.getFormat());
        System.out.println("PublicKey  length: " + publicKey.getEncoded().length + " algorithm: " + publicKey.getAlgorithm() + " format: " + publicKey.getFormat());
        // generate cipher for encryption
        System.out.println("\ninitialize cipher for encryption");
        ParametersWithRandom param = new ParametersWithRandom(mcEliecePublicKey, keyRandom);
        McElieceFujisakiCipher mcElieceFujisakiDigestCipher = new McElieceFujisakiCipher();
        mcElieceFujisakiDigestCipher.init(true, param);
        // random plaintext
        byte[] plaintext = new byte[52428800]; // 50 mb, 50 * 1024 * 1024
        new Random().nextBytes(plaintext);
        System.out.println("pt length:    " + plaintext.length + " (" + (plaintext.length / (1024 * 1024)) + " mb)");
        byte[] ciphertext = mcElieceFujisakiDigestCipher.messageEncrypt(plaintext);
        System.out.println("ct length:    " + ciphertext.length + " (" + (ciphertext.length / (1024 * 1024)) + " mb)");
        System.out.println("\ninitialize cipher for decryption");
        mcElieceFujisakiDigestCipher.init(false, mcEliecePrivateKey);
        byte[] decryptedtext = mcElieceFujisakiDigestCipher.messageDecrypt(ciphertext);
        System.out.println("dt length:    " + decryptedtext.length + " (" + (decryptedtext.length / (1024 * 1024)) + " mb)");
        System.out.println("\ncompare plaintext<-> decryptedtext: " + Arrays.equals(plaintext, decryptedtext));
    }
}

Public 密钥检索 class 总统 James K. Polk,可在 MIT-Licence 下获得:

package com.github.jameskpolk;

import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
import org.bouncycastle.crypto.params.ParametersWithRandom;
import org.bouncycastle.pqc.crypto.mceliece.*;
import org.bouncycastle.pqc.jcajce.provider.mceliece.BCMcElieceCCA2PrivateKey;
import org.bouncycastle.pqc.jcajce.provider.mceliece.BCMcElieceCCA2PublicKey;
import org.bouncycastle.pqc.math.linearalgebra.*;

import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;

public class McElieceRecoverPublicFromPrivate {
    private static final SecureRandom RAND = new SecureRandom();

    public static AsymmetricCipherKeyPair generateKeyPair() {
        McElieceCCA2KeyPairGenerator kpg = new McElieceCCA2KeyPairGenerator();
        McElieceCCA2Parameters params = new McElieceCCA2Parameters();
        McElieceCCA2KeyGenerationParameters genParam = new McElieceCCA2KeyGenerationParameters(RAND, params);
        kpg.init(genParam);
        return kpg.generateKeyPair();
    }

    public static McElieceCCA2PublicKeyParameters recoverPubFromPriv(McElieceCCA2PrivateKeyParameters priv) {
        GF2mField field = priv.getField();
        PolynomialGF2mSmallM gp = priv.getGoppaPoly();
        GF2Matrix h = GoppaCode.createCanonicalCheckMatrix(field, gp);
        Permutation p = priv.getP();
        GF2Matrix hp = (GF2Matrix) h.rightMultiply(p);
        GF2Matrix sInv = hp.getLeftSubMatrix();
        GF2Matrix s = (GF2Matrix) sInv.computeInverse();
        GF2Matrix shp = (GF2Matrix)s.rightMultiply(hp);
        GF2Matrix m = shp.getRightSubMatrix();

        GoppaCode.MaMaPe mmp = new GoppaCode.MaMaPe(sInv, m, p);
        GF2Matrix shortH = mmp.getSecondMatrix();
        GF2Matrix shortG = (GF2Matrix) shortH.computeTranspose();
        // generate public key
        return new McElieceCCA2PublicKeyParameters(
                priv.getN(), gp.getDegree(), shortG,
                priv.getDigest());
    }

    public static void main(String[] args) throws Exception{

        // generate a McEliece key pair

        AsymmetricCipherKeyPair bcKeyPair = generateKeyPair();
        McElieceCCA2PrivateKeyParameters bcPriv = (McElieceCCA2PrivateKeyParameters) bcKeyPair.getPrivate();
        BCMcElieceCCA2PrivateKey priv = new BCMcElieceCCA2PrivateKey(bcPriv);

        // get the first public key

        McElieceCCA2PublicKeyParameters bcPub1 = (McElieceCCA2PublicKeyParameters) bcKeyPair.getPublic();
        BCMcElieceCCA2PublicKey pub1 = new BCMcElieceCCA2PublicKey(bcPub1);

        // Now generate a second public key for the private key

        McElieceCCA2PublicKeyParameters bcPub2 = recoverPubFromPriv(bcPriv);
        BCMcElieceCCA2PublicKey pub2 = new BCMcElieceCCA2PublicKey(bcPub2);

        // print some info about sizes

        System.out.printf("Size of encrypted messages in bits(bytes): %d(%d)\n",
                priv.getEncoded().length, priv.getEncoded().length / 8);
        System.out.printf("private key length: %d\n", bcPriv.getK());
        System.out.printf("public key1 length: %d\n", pub1.getEncoded().length);
        System.out.printf("public key2 length: %d\n", pub2.getEncoded().length);

        // now encrypt different messages with each public key.

        String message1 = "Deposits should be made to account # 3.1415929";
        String message2 = "Deposits should be made to account # 2.71828";

        ParametersWithRandom params1 = new ParametersWithRandom(bcPub1, RAND);
        ParametersWithRandom params2 = new ParametersWithRandom(bcPub2, RAND);

        McElieceFujisakiCipher mcElieceFujisakiDigestCipher1 = new McElieceFujisakiCipher();
        McElieceFujisakiCipher mcElieceFujisakiDigestCipher2 = new McElieceFujisakiCipher();
        mcElieceFujisakiDigestCipher1.init(true, params1);
        mcElieceFujisakiDigestCipher2.init(true, params2);

        byte[] ciphertext1 = mcElieceFujisakiDigestCipher1.messageEncrypt(message1.getBytes(StandardCharsets.UTF_8));
        byte[] ciphertext2 = mcElieceFujisakiDigestCipher2.messageEncrypt(message2.getBytes(StandardCharsets.UTF_8));
        System.out.println("ct1 length:    " + ciphertext1.length + " (" + (ciphertext1.length / (1024 * 1024)) + " mb)");
        System.out.println("ct2 length:    " + ciphertext2.length + " (" + (ciphertext2.length / (1024 * 1024)) + " mb)");

        mcElieceFujisakiDigestCipher1.init(false, bcPriv);
        mcElieceFujisakiDigestCipher2.init(false, bcPriv);

        byte[] decryptedtext1 = mcElieceFujisakiDigestCipher1.messageDecrypt(ciphertext1);
        byte[] decryptedtext2 = mcElieceFujisakiDigestCipher2.messageDecrypt(ciphertext2);

        System.out.printf("Decrypted message 1: %s\n", new String(decryptedtext1, StandardCharsets.UTF_8));
        System.out.printf("Decrypted message 2: %s\n", new String(decryptedtext2, StandardCharsets.UTF_8));

    }
}

I know, that standard RSA supports just a small data blocks to encrypt,

这就是我们使用 hybrid cryptosystem 的原因。数据使用对称密码(数据密钥)加密,对称数据密钥使用非对称密码加密。

I do not need changes detection, I need to make changes impossible (ok, as hard as possible).

如果您无法强制执行任何只读 input/filesystem,那么检测更改是您能做的最好的事情。要么是解密失败,要么是签名失败。

实际上,为了确保数据完整性,我真的会使用签名,而不是纯加密。我知道你不想要那个,但最后它会在那里。一些密码/密码模式是可延展的 - 即使加密并且没有任何身份验证(mac,签名)也可以更改数据,解密是有效的,您将无法检测到完整性失败。

如果您只是依靠应用程序来检测解密失败后数据是否已损坏,那么您正在创建一个完美的解密 oracle(破坏安全性)

I suppose, that the easiest way to do that, is to encrypt data with some secret key and to include the public key in the app, so app could decrypt and show the data, but without the secret key users would not be able to change the content.

在您的应用中硬编码的任何内容都可以视为 revealed/public。您正确识别了风险。如果您有专门的用户,则没有什么可以阻止用户更改应用程序中的密钥并传递无效数据。所以 - 对于在客户端运行的任何东西,您可以使完整性更强,但不是完美的。最后 - 你必须对对手的能力做出一些假设。

I encrypt data with my private key, pass to app, app in runtime decrypts data with it's public key and

理论上(数学上)你可以做到这一点,但大多数当前的库不会让你以错误的方式使用密钥对(私钥用于解密或签名,public 用于加密或验证).如果你想自己编写这样的解决方案,你就有可能产生你可能没有意识到的弱点(适当的填充、时间……)

我认为该方案甚至存在一些弱点(使用私钥加密),但我记不起细节,有人对该主题有更深入的了解(例如评论中的 James Polk)

编辑:

创建签名或 MAC 的示例:https://docs.oracle.com/javase/7/docs/technotes/guides/security/crypto/CryptoSpec.html

顺便说一句 - 使用 aes-gcm Java 密码实现自动将 mac 标签附加到密文的末尾

正如@President James K. Polk 在他的一篇评论中所说,在我看来,唯一的解决方案是签署只读数据并使用仅当数据经过验证。在您的 "Possible solution & attack" 部分中,您写道该程序比较了一些很容易被覆盖的数字。通常签名是用 (SHA256-) 数据的散列,但您可以签署完整的数据而无需先对其进行散列,并且 4 KB 的数据不会在我的桌面上带来性能问题 Java。

我设置了一个完整的工作示例来模拟维护者端和应用程序端,作为一个小东西,我用 AES CBC(从签名生成的密钥)加密了明文。我知道这种加密模式不是 "best way",因为数据不需要完全保密但不直接可见,这是一个很好的解决方案。

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.*;
import java.util.Arrays;
import java.util.Random;

public class Cyptosystem {
    public static void main(String[] args) throws NoSuchAlgorithmException, SignatureException, InvalidKeyException, NoSuchPaddingException, InvalidAlgorithmParameterException, BadPaddingException, IllegalBlockSizeException {
        System.out.println("Cryptosystem for \n");
        System.out.println("Warning: this program is experimental and has no proper exception handling");
        byte[] plaintext = new byte[4000]; // content to get secured, provided by maintainers
        byte[] ciphertext = new byte[0]; // encryped plaintext
        byte[] dataForApp = new byte[0]; // initvector | ciphertext
        new Random().nextBytes(plaintext);

        // generate rsa keypair
        System.out.println("generate the RSA keypair");
        KeyPairGenerator rsaGenerator = KeyPairGenerator.getInstance("RSA");
        SecureRandom random = new SecureRandom();
        rsaGenerator.initialize(4096, random);
        KeyPair rsaKeyPair = rsaGenerator.generateKeyPair();
        PrivateKey rsaPrivateKey = rsaKeyPair.getPrivate(); // for signature
        PublicKey rsaPublicKey = rsaKeyPair.getPublic(); // for verification, implemented in app resources

        System.out.println("sign & encrypt the plaintext");
        // signature done by maintainers
        Signature sig = Signature.getInstance("SHA256withRSA");
        sig.initSign(rsaPrivateKey);
        sig.update(plaintext);
        byte[] signature = sig.sign(); // provide to app as byte array, hexstring or base64 as you like
        // encrypt plaintext with signature
        byte[] initvector = new byte[16];
        SecureRandom secureRandom = new SecureRandom();
        secureRandom.nextBytes(initvector); // random initvector
        // you can use another aes mode for encryption e.g. gcm
        // you can use a hmac as key derivation ...
        // i'm using sha256 to get a 32 byte long key
        MessageDigest md = MessageDigest.getInstance("SHA-256");
        byte[] aeskey = md.digest(signature);
        SecretKeySpec keySpec = new SecretKeySpec(aeskey, "AES");
        IvParameterSpec ivKeySpec = new IvParameterSpec(initvector);
        Cipher aesCipherEnc = Cipher.getInstance("AES/CBC/PKCS5PADDING");
        aesCipherEnc.init(Cipher.ENCRYPT_MODE, keySpec, ivKeySpec);
        ciphertext = aesCipherEnc.doFinal(plaintext);
        // copy iv | ciphertext
        dataForApp = new byte[ciphertext.length + 16]; // initvector length 16 byte
        System.arraycopy(initvector, 0, dataForApp, 0, initvector.length);
        System.arraycopy(ciphertext, 0, dataForApp, initvector.length, ciphertext.length);
        // send the dataForApp to the app (as byte array, hex string, base64 as you like
        System.out.println("dataForApp length: " + dataForApp.length);

        // app side, receive dataForApp & signature, already has public key
        byte[] dataForAppApp = dataForApp.clone();
        byte[] signatureApp = signature.clone();
        System.out.println("decrypt and verify the signature");
        // get initvector & ciphertext
        byte[] initvectorApp = new byte[16];
        byte[] ciphertextApp = new byte[(dataForAppApp.length - 16)];
        System.arraycopy(dataForAppApp, 0, initvectorApp, 0, 16);
        System.arraycopy(dataForAppApp,16, ciphertextApp, 0, (dataForAppApp.length - 16));
        // decrypt data
        MessageDigest mdApp = MessageDigest.getInstance("SHA-256");
        byte[] aeskeyApp = md.digest(signature);
        SecretKeySpec keySpecApp = new SecretKeySpec(aeskeyApp, "AES");
        IvParameterSpec ivKeySpecApp = new IvParameterSpec(initvectorApp);
        Cipher aesCipherDec = Cipher.getInstance("AES/CBC/PKCS5PADDING");
        aesCipherDec.init(Cipher.DECRYPT_MODE, keySpecApp, ivKeySpecApp);
        byte[] decrypttext = aesCipherDec.doFinal(ciphertextApp);
        System.out.println("plaintext equals decrypttext: " +  Arrays.equals(decrypttext, plaintext));
        // don't use the ciphertext as the signature is not verified
        Signature sigApp = Signature.getInstance("SHA256withRSA");
        sigApp.initVerify(rsaPublicKey);
        sigApp.update(decrypttext);
        boolean signatureVerified = sigApp.verify(signatureApp);
        System.out.println("signatureApp verified: " + signatureVerified);
        System.out.println("if verified == true we can use the decrypttext");
    }
}