Java 安全速查表¶
Java 中的注入防护¶
本节旨在提供在 Java 应用程序代码中处理注入的提示。
提示中使用的示例代码位于此处。
什么是注入¶
OWASP Top 10 中将注入定义如下
考虑任何可以向系统发送不可信数据的人,包括外部用户、内部用户和管理员。
防止注入的一般建议¶
以下几点可以普遍应用于防止注入问题
- 对用户输入/输出应用输入验证(使用白名单方法)并结合输出清理+转义。
- 如果您需要与系统交互,请尝试使用您的技术栈(Java / .Net / PHP...)提供的 API 功能,而不是构建命令。
此速查表中提供了更多建议。
特定注入类型¶
本节中的示例将以 Java 技术提供(参见相关的 Maven 项目),但建议适用于其他技术,如 .Net / PHP / Ruby / Python...
SQL¶
症状¶
当应用程序使用不可信的用户输入通过字符串构建并执行 SQL 查询时,会发生此类注入。
如何防止¶
使用查询参数化来防止注入。
示例¶
/*No DB framework used here in order to show the real use of
Prepared Statement from Java API*/
/*Open connection with H2 database and use it*/
Class.forName("org.h2.Driver");
String jdbcUrl = "jdbc:h2:file:" + new File(".").getAbsolutePath() + "/target/db";
try (Connection con = DriverManager.getConnection(jdbcUrl)) {
/* Sample A: Select data using Prepared Statement*/
String query = "select * from color where friendly_name = ?";
List<String> colors = new ArrayList<>();
try (PreparedStatement pStatement = con.prepareStatement(query)) {
pStatement.setString(1, "yellow");
try (ResultSet rSet = pStatement.executeQuery()) {
while (rSet.next()) {
colors.add(rSet.getString(1));
}
}
}
/* Sample B: Insert data using Prepared Statement*/
query = "insert into color(friendly_name, red, green, blue) values(?, ?, ?, ?)";
int insertedRecordCount;
try (PreparedStatement pStatement = con.prepareStatement(query)) {
pStatement.setString(1, "orange");
pStatement.setInt(2, 239);
pStatement.setInt(3, 125);
pStatement.setInt(4, 11);
insertedRecordCount = pStatement.executeUpdate();
}
/* Sample C: Update data using Prepared Statement*/
query = "update color set blue = ? where friendly_name = ?";
int updatedRecordCount;
try (PreparedStatement pStatement = con.prepareStatement(query)) {
pStatement.setInt(1, 10);
pStatement.setString(2, "orange");
updatedRecordCount = pStatement.executeUpdate();
}
/* Sample D: Delete data using Prepared Statement*/
query = "delete from color where friendly_name = ?";
int deletedRecordCount;
try (PreparedStatement pStatement = con.prepareStatement(query)) {
pStatement.setString(1, "orange");
deletedRecordCount = pStatement.executeUpdate();
}
}
参考资料¶
JPA¶
症状¶
当应用程序使用不可信的用户输入通过字符串构建并执行 JPA 查询时,会发生此类注入。这与 SQL 注入非常相似,但这里被篡改的语言不是 SQL 而是 JPA QL。
如何防止¶
使用 Java 持久化查询语言的查询参数化来防止注入。
示例¶
EntityManager entityManager = null;
try {
/* Get a ref on EntityManager to access DB */
entityManager = Persistence.createEntityManagerFactory("testJPA").createEntityManager();
/* Define parameterized query prototype using named parameter to enhance readability */
String queryPrototype = "select c from Color c where c.friendlyName = :colorName";
/* Create the query, set the named parameter and execute the query */
Query queryObject = entityManager.createQuery(queryPrototype);
Color c = (Color) queryObject.setParameter("colorName", "yellow").getSingleResult();
} finally {
if (entityManager != null && entityManager.isOpen()) {
entityManager.close();
}
}
参考¶
操作系统¶
症状¶
当应用程序使用不可信的用户输入通过字符串构建并执行操作系统命令时,会发生此类注入。
如何防止¶
使用技术栈API来防止注入。
示例¶
/* The context taken is, for example, to perform a PING against a computer.
* The prevention is to use the feature provided by the Java API instead of building
* a system command as String and execute it */
InetAddress host = InetAddress.getByName("localhost");
var reachable = host.isReachable(5000);
参考¶
XML: XPath 注入¶
症状¶
当应用程序使用不可信的用户输入通过字符串构建并执行 XPath 查询时,会发生此类注入。
如何防止¶
使用 XPath 变量解析器来防止注入。
示例¶
变量解析器实现。
/**
* Resolver in order to define parameter for XPATH expression.
*
*/
public class SimpleVariableResolver implements XPathVariableResolver {
private final Map<QName, Object> vars = new HashMap<QName, Object>();
/**
* External methods to add parameter
*
* @param name Parameter name
* @param value Parameter value
*/
public void addVariable(QName name, Object value) {
vars.put(name, value);
}
/**
* {@inheritDoc}
*
* @see javax.xml.xpath.XPathVariableResolver#resolveVariable(javax.xml.namespace.QName)
*/
public Object resolveVariable(QName variableName) {
return vars.get(variableName);
}
}
使用其执行 XPath 查询的代码。
/*Create a XML document builder factory*/
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
/*Disable External Entity resolution for different cases*/
//Do not performed here in order to focus on variable resolver code
//but do it for production code !
/*Load XML file*/
DocumentBuilder builder = dbf.newDocumentBuilder();
Document doc = builder.parse(new File("src/test/resources/SampleXPath.xml"));
/* Create and configure parameter resolver */
String bid = "bk102";
SimpleVariableResolver variableResolver = new SimpleVariableResolver();
variableResolver.addVariable(new QName("bookId"), bid);
/*Create and configure XPATH expression*/
XPath xpath = XPathFactory.newInstance().newXPath();
xpath.setXPathVariableResolver(variableResolver);
XPathExpression xPathExpression = xpath.compile("//book[@id=$bookId]");
/* Apply expression on XML document */
Object nodes = xPathExpression.evaluate(doc, XPathConstants.NODESET);
NodeList nodesList = (NodeList) nodes;
Element book = (Element)nodesList.item(0);
var containsRalls = book.getTextContent().contains("Ralls, Kim");
参考¶
HTML/JavaScript/CSS¶
症状¶
当应用程序使用不可信的用户输入构建 HTTP 响应并将其发送到浏览器时,会发生此类注入。
如何防止¶
要么应用严格的输入验证(白名单方法),要么在输入验证不可行时使用输出清理+转义(如果可能,始终将两者结合起来)。
示例¶
/*
INPUT WAY: Receive data from user
Here it's recommended to use strict input validation using allowlist approach.
In fact, you ensure that only allowed characters are part of the input received.
*/
String userInput = "You user login is owasp-user01";
/* First we check that the value contains only expected character*/
if (!Pattern.matches("[a-zA-Z0-9\\s\\-]{1,50}", userInput))
{
return false;
}
/* If the first check pass then ensure that potential dangerous character
that we have allowed for business requirement are not used in a dangerous way.
For example here we have allowed the character '-', and, this can
be used in SQL injection so, we
ensure that this character is not used is a continuous form.
Use the API COMMONS LANG v3 to help in String analysis...
*/
If (0 != StringUtils.countMatches(userInput.replace(" ", ""), "--"))
{
return false;
}
/*
OUTPUT WAY: Send data to user
Here we escape + sanitize any data sent to user
Use the OWASP Java HTML Sanitizer API to handle sanitizing
Use the OWASP Java Encoder API to handle HTML tag encoding (escaping)
*/
String outputToUser = "You <p>user login</p> is <strong>owasp-user01</strong>";
outputToUser += "<script>alert(22);</script><img src='#' onload='javascript:alert(23);'>";
/* Create a sanitizing policy that only allow tag '<p>' and '<strong>'*/
PolicyFactory policy = new HtmlPolicyBuilder().allowElements("p", "strong").toFactory();
/* Sanitize the output that will be sent to user*/
String safeOutput = policy.sanitize(outputToUser);
/* Encode HTML Tag*/
safeOutput = Encode.forHtml(safeOutput);
String finalSafeOutputExpected = "You <p>user login</p> is <strong>owasp-user01</strong>";
if (!finalSafeOutputExpected.equals(safeOutput))
{
return false;
}
参考¶
LDAP¶
已创建了一个专门的 速查表。
NoSQL¶
症状¶
当应用程序使用不可信的用户输入构建 NoSQL API 调用表达式时,会发生此类注入。
如何防止¶
由于存在许多 NoSQL 数据库系统,并且每个系统都使用 API 进行调用,因此务必确保接收到的用于构建 API 调用表达式的用户输入不包含在目标 API 语法中具有特殊含义的任何字符。这是为了避免其被用于转义初始调用表达式,从而根据精心制作的用户输入创建另一个表达式。同样重要的是,不要使用字符串连接来构建 API 调用表达式,而应使用 API 来创建表达式。
示例 - MongoDB¶
/* Here use MongoDB as target NoSQL DB */
String userInput = "Brooklyn";
/* First ensure that the input do no contains any special characters
for the current NoSQL DB call API,
here they are: ' " \ ; { } $
*/
//Avoid regexp this time in order to made validation code
//more easy to read and understand...
ArrayList < String > specialCharsList = new ArrayList < String > () {
{
add("'");
add("\"");
add("\\");
add(";");
add("{");
add("}");
add("$");
}
};
for (String specChar: specialCharsList) {
if (userInput.contains(specChar)) {
return false;
}
}
//Add also a check on input max size
if (!userInput.length() <= 50)
{
return false;
}
/* Then perform query on database using API to build expression */
//Connect to the local MongoDB instance
try(MongoClient mongoClient = new MongoClient()){
MongoDatabase db = mongoClient.getDatabase("test");
//Use API query builder to create call expression
//Create expression
Bson expression = eq("borough", userInput);
//Perform call
FindIterable<org.bson.Document> restaurants = db.getCollection("restaurants").find(expression);
//Verify result consistency
restaurants.forEach(new Block<org.bson.Document>() {
@Override
public void apply(final org.bson.Document doc) {
String restBorough = (String)doc.get("borough");
if (!"Brooklyn".equals(restBorough))
{
return false;
}
}
});
}
参考¶
日志注入¶
症状¶
当应用程序在应用程序日志消息中包含不可信数据时(例如,如果攻击者可以在不可信数据中注入 CRLF 字符,他们可以导致额外的日志条目看起来像是来自完全不同的用户),就会发生日志注入。有关此攻击的更多信息可在 OWASP 日志注入页面找到。
如何防止¶
为了防止攻击者将恶意内容写入应用程序日志,请应用以下防御措施,例如
- 使用结构化日志格式(如 JSON),而不是非结构化文本格式。非结构化格式容易受到回车符(CR)和换行符(LF)注入的影响(参见CWE-93)。
- 限制用于创建日志消息的用户输入值的大小。
- 在 Web 浏览器中查看日志文件时,请确保应用了所有 XSS 防御措施。
使用 Log4j Core 2 的示例¶
生产环境推荐的日志策略是使用 Log4j 2.14.0 中引入的结构化 JSON 模板布局将日志发送到网络套接字,并使用 maxStringLength
配置属性将字符串大小限制为 500 字节
<?xml version="1.0" encoding="UTF-8"?>
<Configuration xmlns="https://logging.apache.ac.cn/xml/ns"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
https://logging.apache.ac.cn/xml/ns
https://logging.apache.ac.cn/xml/ns/log4j-config-2.xsd">
<Appenders>
<Socket name="SOCKET"
host="localhost"
port="12345">
<!-- Limit the size of any string field in the produced JSON document to 500 bytes -->
<JsonTemplateLayout maxStringLength="500"
nullEventDelimiterEnabled="true"/>
</Socket>
</Appenders>
<Loggers>
<Root level="DEBUG">
<AppenderRef ref="SOCKET"/>
</Root>
</Loggers>
</Configuration>
有关更多提示,请参阅 Log4j 网站上的与面向服务的架构集成。
代码层面的日志器使用
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
...
// Most common way to declare a logger
private static final LOGGER = LogManager.getLogger();
// GOOD!
//
// Use parameterized logging to add user data to a message
// The pattern should be a compile-time constant
logger.warn("Login failed for user {}.", username);
// BAD!
//
// Don't mix string concatenation and parameters
// If `username` contains `{}`, the exception will leak into the message
logger.warn("Failure for user " + username + " and role {}.", role, ex);
...
有关更多信息,请参阅Log4j API 最佳实践。
使用 Logback 的示例¶
生产环境推荐的日志策略是使用 Logback 1.3.8 中引入的结构化 JsonEncoder。在以下示例中,Logback 配置为滚动生成 10 个每个 5 MiB 的日志文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration>
<configuration>
<import class="ch.qos.logback.classic.encoder.JsonEncoder"/>
<import class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy"/>
<import class="ch.qos.logback.core.rolling.RollingFileAppender"/>
<import class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy"/>
<appender name="RollingFile" class="RollingFileAppender">
<file>app.log</file>
<rollingPolicy class="FixedWindowRollingPolicy">
<fileNamePattern>app-%i.log</fileNamePattern>
<minIndex>1</minIndex>
<maxIndex>10</maxIndex>
</rollingPolicy>
<triggeringPolicy class="SizeBasedTriggeringPolicy">
<maxFileSize>5MB</maxFileSize>
</triggeringPolicy>
<encoder class="JsonEncoder"/>
</appender>
<root level="DEBUG">
<appender-ref ref="SOCKET"/>
</root>
</configuration>
代码层面的日志器使用
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
...
// Most common way to declare a logger
Logger logger = LoggerFactory.getLogger(MyClass.class);
// GOOD!
//
// Use parameterized logging to add user data to a message
// The pattern should be a compile-time constant
logger.warn("Login failed for user {}.", username);
// BAD!
//
// Don't mix string concatenation and parameters
// If `username` contains `{}`, the exception will leak into the message
logger.warn("Failure for user " + username + " and role {}.", role, ex);
...
参考¶
密码学¶
一般密码学指南¶
- 永远不要自己编写密码函数。
- 在可能的情况下,尽量避免编写任何密码学代码。相反,尝试使用现有的密钥管理解决方案或云提供商提供的密钥管理解决方案。有关更多信息,请参阅 OWASP 密钥管理速查表。
- 如果您无法使用现有的密钥管理解决方案,请尝试使用受信任且知名的实现库,而不是使用 JCA/JCE 内置的库,因为使用它们很容易犯密码错误。
- 确保您的应用程序或协议能够轻松支持未来的密码算法更改。
- 尽可能使用您的包管理器来保持所有包的最新。关注您的开发设置上的更新,并相应地计划应用程序的更新。
- 我们将在下面展示基于 Google Tink 的示例,Google Tink 是一个由密码学专家创建的库,旨在安全地使用密码学(在最大程度地减少使用标准密码学库时常见的错误方面)。
存储加密¶
遵循 OWASP 密码存储速查表中的算法指南。
使用 Google Tink 的对称加密示例¶
Google Tink 提供了执行常见任务的文档。
例如,此页面(来自 Google 网站)展示了如何执行简单的对称加密。
以下代码片段展示了此功能的封装使用
点击此处查看“Tink 对称加密”代码片段。
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.crypto.tink.Aead;
import com.google.crypto.tink.InsecureSecretKeyAccess;
import com.google.crypto.tink.KeysetHandle;
import com.google.crypto.tink.TinkJsonProtoKeysetFormat;
import com.google.crypto.tink.aead.AeadConfig;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Base64;
// AesGcmSimpleTest
public class App {
// Based on example from:
// https://github.com/tink-crypto/tink-java/tree/main/examples/aead
public static void main(String[] args) throws Exception {
// Key securely generated using:
// tinkey create-keyset --key-template AES128_GCM --out-format JSON --out aead_test_keyset.json
// Register all AEAD key types with the Tink runtime.
AeadConfig.register();
// Read the keyset into a KeysetHandle.
KeysetHandle handle =
TinkJsonProtoKeysetFormat.parseKeyset(
new String(Files.readAllBytes( Paths.get("/home/fredbloggs/aead_test_keyset.json")), UTF_8), InsecureSecretKeyAccess.get());
String message = "This message to be encrypted";
System.out.println(message);
// Add some relevant context about the encrypted data that should be verified
// on decryption
String metadata = "Sender: [email protected]";
// Encrypt the message
byte[] cipherText = AesGcmSimple.encrypt(message, metadata, handle);
System.out.println(Base64.getEncoder().encodeToString(cipherText));
// Decrypt the message
String message2 = AesGcmSimple.decrypt(cipherText, metadata, handle);
System.out.println(message2);
}
}
class AesGcmSimple {
public static byte[] encrypt(String plaintext, String metadata, KeysetHandle handle) throws Exception {
// Get the primitive.
Aead aead = handle.getPrimitive(Aead.class);
return aead.encrypt(plaintext.getBytes(UTF_8), metadata.getBytes(UTF_8));
}
public static String decrypt(byte[] ciphertext, String metadata, KeysetHandle handle) throws Exception {
// Get the primitive.
Aead aead = handle.getPrimitive(Aead.class);
return new String(aead.decrypt(ciphertext, metadata.getBytes(UTF_8)),UTF_8);
}
}
使用内置 JCA/JCE 类的对称加密示例¶
如果您绝对不能使用单独的库,仍然可以使用内置的 JCA/JCE 类,但强烈建议让密码学专家审查完整的设计和代码,因为即使最微小的错误也可能严重削弱您的加密。
以下代码片段展示了使用 AES-GCM 执行数据加密/解密的示例。
此代码的一些限制/陷阱
- 它没有考虑密钥轮换或管理,这本身就是一个完整的话题。
- 每次加密操作使用不同的 nonce 非常重要,特别是如果使用相同的密钥。有关更多信息,请参阅 Cryptography Stack Exchange 上的此回答。
- 密钥需要安全存储。
点击此处查看“JCA/JCE 对称加密”代码片段。
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import javax.crypto.spec.*;
import javax.crypto.*;
import java.util.Base64;
// AesGcmSimpleTest
class Main {
public static void main(String[] args) throws Exception {
// Key of 32 bytes / 256 bits for AES
KeyGenerator keyGen = KeyGenerator.getInstance(AesGcmSimple.ALGORITHM);
keyGen.init(AesGcmSimple.KEY_SIZE, new SecureRandom());
SecretKey secretKey = keyGen.generateKey();
// Nonce of 12 bytes / 96 bits and this size should always be used.
// It is critical for AES-GCM that a unique nonce is used for every cryptographic operation.
byte[] nonce = new byte[AesGcmSimple.IV_LENGTH];
SecureRandom random = new SecureRandom();
random.nextBytes(nonce);
var message = "This message to be encrypted";
System.out.println(message);
// Encrypt the message
byte[] cipherText = AesGcmSimple.encrypt(message, nonce, secretKey);
System.out.println(Base64.getEncoder().encodeToString(cipherText));
// Decrypt the message
var message2 = AesGcmSimple.decrypt(cipherText, nonce, secretKey);
System.out.println(message2);
}
}
class AesGcmSimple {
public static final String ALGORITHM = "AES";
public static final String CIPHER_ALGORITHM = "AES/GCM/NoPadding";
public static final int KEY_SIZE = 256;
public static final int TAG_LENGTH = 128;
public static final int IV_LENGTH = 12;
public static byte[] encrypt(String plaintext, byte[] nonce, SecretKey secretKey) throws Exception {
return cryptoOperation(plaintext.getBytes(StandardCharsets.UTF_8), nonce, secretKey, Cipher.ENCRYPT_MODE);
}
public static String decrypt(byte[] ciphertext, byte[] nonce, SecretKey secretKey) throws Exception {
return new String(cryptoOperation(ciphertext, nonce, secretKey, Cipher.DECRYPT_MODE), StandardCharsets.UTF_8);
}
private static byte[] cryptoOperation(byte[] text, byte[] nonce, SecretKey secretKey, int mode) throws Exception {
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(TAG_LENGTH, nonce);
cipher.init(mode, secretKey, gcmParameterSpec);
return cipher.doFinal(text);
}
}
传输加密¶
同样,遵循 OWASP 密码存储速查表中的算法指南。
使用 Google Tink 的非对称加密示例¶
Google Tink 提供了执行常见任务的文档。
例如,此页面(来自 Google 网站)展示了如何执行混合加密过程,其中双方希望根据其非对称密钥对共享数据。
以下代码片段展示了此功能如何用于在 Alice 和 Bob 之间共享秘密
点击此处查看“Tink 混合加密”代码片段。
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.crypto.tink.HybridDecrypt;
import com.google.crypto.tink.HybridEncrypt;
import com.google.crypto.tink.InsecureSecretKeyAccess;
import com.google.crypto.tink.KeysetHandle;
import com.google.crypto.tink.TinkJsonProtoKeysetFormat;
import com.google.crypto.tink.hybrid.HybridConfig;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Base64;
// HybridReplaceTest
class App {
public static void main(String[] args) throws Exception {
/*
Generated public/private keypairs for Bob and Alice using the
following tinkey commands:
./tinkey create-keyset \
--key-template DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_256_GCM \
--out-format JSON --out alice_private_keyset.json
./tinkey create-keyset \
--key-template DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_256_GCM \
--out-format JSON --out bob_private_keyset.json
./tinkey create-public-keyset --in alice_private_keyset.json \
--in-format JSON --out-format JSON --out alice_public_keyset.json
./tinkey create-public-keyset --in bob_private_keyset.json \
--in-format JSON --out-format JSON --out bob_public_keyset.json
*/
HybridConfig.register();
// Generate ECC key pair for Alice
var alice = new HybridSimple(
getKeysetHandle("/home/alicesmith/private_keyset.json"),
getKeysetHandle("/home/alicesmith/public_keyset.json")
);
KeysetHandle alicePublicKey = alice.getPublicKey();
// Generate ECC key pair for Bob
var bob = new HybridSimple(
getKeysetHandle("/home/bobjones/private_keyset.json"),
getKeysetHandle("/home/bobjones/public_keyset.json")
);
KeysetHandle bobPublicKey = bob.getPublicKey();
// This keypair generation should be reperformed every so often in order to
// obtain a new shared secret to avoid a long lived shared secret.
// Alice encrypts a message to send to Bob
String plaintext = "Hello, Bob!";
// Add some relevant context about the encrypted data that should be verified
// on decryption
String metadata = "Sender: [email protected]";
System.out.println("Secret being sent from Alice to Bob: " + plaintext);
var cipherText = alice.encrypt(bobPublicKey, plaintext, metadata);
System.out.println("Ciphertext being sent from Alice to Bob: " + Base64.getEncoder().encodeToString(cipherText));
// Bob decrypts the message
var decrypted = bob.decrypt(cipherText, metadata);
System.out.println("Secret received by Bob from Alice: " + decrypted);
System.out.println();
// Bob encrypts a message to send to Alice
String plaintext2 = "Hello, Alice!";
// Add some relevant context about the encrypted data that should be verified
// on decryption
String metadata2 = "Sender: [email protected]";
System.out.println("Secret being sent from Bob to Alice: " + plaintext2);
var cipherText2 = bob.encrypt(alicePublicKey, plaintext2, metadata2);
System.out.println("Ciphertext being sent from Bob to Alice: " + Base64.getEncoder().encodeToString(cipherText2));
// Bob decrypts the message
var decrypted2 = alice.decrypt(cipherText2, metadata2);
System.out.println("Secret received by Alice from Bob: " + decrypted2);
}
private static KeysetHandle getKeysetHandle(String filename) throws Exception
{
return TinkJsonProtoKeysetFormat.parseKeyset(
new String(Files.readAllBytes( Paths.get(filename)), UTF_8), InsecureSecretKeyAccess.get());
}
}
class HybridSimple {
private KeysetHandle privateKey;
private KeysetHandle publicKey;
public HybridSimple(KeysetHandle privateKeyIn, KeysetHandle publicKeyIn) throws Exception {
privateKey = privateKeyIn;
publicKey = publicKeyIn;
}
public KeysetHandle getPublicKey() {
return publicKey;
}
public byte[] encrypt(KeysetHandle partnerPublicKey, String message, String metadata) throws Exception {
HybridEncrypt encryptor = partnerPublicKey.getPrimitive(HybridEncrypt.class);
// return the encrypted value
return encryptor.encrypt(message.getBytes(UTF_8), metadata.getBytes(UTF_8));
}
public String decrypt(byte[] ciphertext, String metadata) throws Exception {
HybridDecrypt decryptor = privateKey.getPrimitive(HybridDecrypt.class);
// return the encrypted value
return new String(decryptor.decrypt(ciphertext, metadata.getBytes(UTF_8)),UTF_8);
}
}
使用内置 JCA/JCE 类的非对称加密示例¶
如果您绝对不能使用单独的库,仍然可以使用内置的 JCA/JCE 类,但强烈建议让密码学专家审查完整的设计和代码,因为即使最微小的错误也可能严重削弱您的加密。
以下代码片段展示了使用椭圆曲线/迪菲-赫尔曼(ECDH)结合 AES-GCM 在两不同方之间执行数据加密/解密的示例,而无需在两方之间传输对称密钥。相反,双方交换公钥,然后可以使用 ECDH 生成一个共享密钥,该密钥可用于对称加密。
请注意,此代码示例依赖于上一节中的 AesGcmSimple 类。
此代码的一些限制/陷阱
- 它没有考虑密钥轮换或管理,这本身就是一个完整的话题。
- 代码故意对每次加密操作强制使用新的 nonce,但这必须作为与密文并列的单独数据项进行管理。
- 私钥需要安全存储。
- 代码未考虑在使用前验证公钥。
- 总的来说,双方之间没有进行真实性验证。
点击此处查看“JCA/JCE 混合加密”代码片段。
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import javax.crypto.spec.*;
import javax.crypto.*;
import java.util.*;
import java.security.*;
import java.security.spec.*;
import java.util.Arrays;
// ECDHSimpleTest
class Main {
public static void main(String[] args) throws Exception {
// Generate ECC key pair for Alice
var alice = new ECDHSimple();
Key alicePublicKey = alice.getPublicKey();
// Generate ECC key pair for Bob
var bob = new ECDHSimple();
Key bobPublicKey = bob.getPublicKey();
// This keypair generation should be reperformed every so often in order to
// obtain a new shared secret to avoid a long lived shared secret.
// Alice encrypts a message to send to Bob
String plaintext = "Hello"; //, Bob!";
System.out.println("Secret being sent from Alice to Bob: " + plaintext);
var retPair = alice.encrypt(bobPublicKey, plaintext);
var nonce = retPair.getKey();
var cipherText = retPair.getValue();
System.out.println("Both cipherText and nonce being sent from Alice to Bob: " + Base64.getEncoder().encodeToString(cipherText) + " " + Base64.getEncoder().encodeToString(nonce));
// Bob decrypts the message
var decrypted = bob.decrypt(alicePublicKey, cipherText, nonce);
System.out.println("Secret received by Bob from Alice: " + decrypted);
System.out.println();
// Bob encrypts a message to send to Alice
String plaintext2 = "Hello"; //, Alice!";
System.out.println("Secret being sent from Bob to Alice: " + plaintext2);
var retPair2 = bob.encrypt(alicePublicKey, plaintext2);
var nonce2 = retPair2.getKey();
var cipherText2 = retPair2.getValue();
System.out.println("Both cipherText2 and nonce2 being sent from Bob to Alice: " + Base64.getEncoder().encodeToString(cipherText2) + " " + Base64.getEncoder().encodeToString(nonce2));
// Bob decrypts the message
var decrypted2 = alice.decrypt(bobPublicKey, cipherText2, nonce2);
System.out.println("Secret received by Alice from Bob: " + decrypted2);
}
}
class ECDHSimple {
private KeyPair keyPair;
public class AesKeyNonce {
public SecretKey Key;
public byte[] Nonce;
}
public ECDHSimple() throws Exception {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
ECGenParameterSpec ecSpec = new ECGenParameterSpec("secp256r1"); // Using secp256r1 curve
keyPairGenerator.initialize(ecSpec);
keyPair = keyPairGenerator.generateKeyPair();
}
public Key getPublicKey() {
return keyPair.getPublic();
}
public AbstractMap.SimpleEntry<byte[], byte[]> encrypt(Key partnerPublicKey, String message) throws Exception {
// Generate the AES Key and Nonce
AesKeyNonce aesParams = generateAESParams(partnerPublicKey);
// return the encrypted value
return new AbstractMap.SimpleEntry<>(
aesParams.Nonce,
AesGcmSimple.encrypt(message, aesParams.Nonce, aesParams.Key)
);
}
public String decrypt(Key partnerPublicKey, byte[] ciphertext, byte[] nonce) throws Exception {
// Generate the AES Key and Nonce
AesKeyNonce aesParams = generateAESParams(partnerPublicKey, nonce);
// return the decrypted value
return AesGcmSimple.decrypt(ciphertext, aesParams.Nonce, aesParams.Key);
}
private AesKeyNonce generateAESParams(Key partnerPublicKey, byte[] nonce) throws Exception {
// Derive the secret based on this side's private key and the other side's public key
KeyAgreement keyAgreement = KeyAgreement.getInstance("ECDH");
keyAgreement.init(keyPair.getPrivate());
keyAgreement.doPhase(partnerPublicKey, true);
byte[] secret = keyAgreement.generateSecret();
AesKeyNonce aesKeyNonce = new AesKeyNonce();
// Copy first 32 bytes as the key
byte[] key = Arrays.copyOfRange(secret, 0, (AesGcmSimple.KEY_SIZE / 8));
aesKeyNonce.Key = new SecretKeySpec(key, 0, key.length, "AES");
// Passed in nonce will be used.
aesKeyNonce.Nonce = nonce;
return aesKeyNonce;
}
private AesKeyNonce generateAESParams(Key partnerPublicKey) throws Exception {
// Nonce of 12 bytes / 96 bits and this size should always be used.
// It is critical for AES-GCM that a unique nonce is used for every cryptographic operation.
// Therefore this is not generated from the shared secret
byte[] nonce = new byte[AesGcmSimple.IV_LENGTH];
SecureRandom random = new SecureRandom();
random.nextBytes(nonce);
return generateAESParams(partnerPublicKey, nonce);
}
}