Start with encryption

This commit is contained in:
Oracle 2026-04-23 19:22:01 +02:00
parent 9df61e0cd3
commit b6af1c9792
Signed by: Oracle
SSH key fingerprint: SHA256:x4/RtnjUyuHkdvmwNDsWSfcfF1V5PNr3OpriZqOvCX8
8 changed files with 413 additions and 79 deletions

View file

@ -6,20 +6,27 @@ import ai.nomyo.util.Pass2Key;
import lombok.Getter;
import javax.crypto.*;
import java.io.FileWriter;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.math.BigInteger;
import java.net.*;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.security.*;
import java.security.spec.RSAKeyGenParameterSpec;
import java.util.Map;
import java.util.Set;
import java.security.spec.*;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.locks.ReentrantLock;
/**
@ -61,19 +68,17 @@ public class SecureCompletionClient {
* Lock for double-checked key initialization.
*/
private final ReentrantLock keyInitLock = new ReentrantLock();
private final HttpClient httpClient;
/**
* RSA private key ({@code null} until loaded/generated).
*/
@Getter
private PrivateKey privateKey;
/**
* PEM-encoded public key ({@code null} until loaded/generated).
*/
@Getter
private String publicPemKey;
/**
* Keys initialized.
*/
@ -98,6 +103,7 @@ public class SecureCompletionClient {
this.useSecureMemory = secureMemory;
this.keySize = Constants.RSA_KEY_SIZE;
this.maxRetries = maxRetries;
this.httpClient = HttpClient.newHttpClient();
}
private static String readFileContent(String filePath) throws IOException {
@ -117,7 +123,7 @@ public class SecureCompletionClient {
String privatePem = PEMConverter.toPEM(pair.getPrivate().getEncoded(), true);
String publicPem = PEMConverter.toPEM(pair.getPublic().getEncoded(), false);
if (saveToFile) {
if (saveToFile) {
Path keyFolder = Path.of(keyDir);
if (!Files.exists(keyFolder)) {
try {
@ -230,9 +236,38 @@ public class SecureCompletionClient {
/**
* GET {@code {routerUrl}/pki/public_key}. Returns server PEM public key.
* Errors are propagated as {@link CompletionException} wrappers around the underlying checked exception.
*/
public CompletableFuture<String> fetchServerPublicKey() {
throw new UnsupportedOperationException("Not yet implemented");
if (!this.routerUrl.startsWith("https://")) {
if (!this.allowHttp) {
return CompletableFuture.failedFuture(new SecurityError("Server public key must be fetched over HTTPS to prevent MITM attacks."));
} else {
System.out.println("Fetching server public key over HTTP (local dev mode)");
}
}
URI url;
try {
url = new URI(this.routerUrl + "/pki/public_key");
} catch (URISyntaxException e) {
return CompletableFuture.failedFuture(new CompletionException("Invalid URI: " + e.getMessage(), e));
}
HttpRequest request = HttpRequest.newBuilder(url).timeout(Duration.of(60, ChronoUnit.SECONDS)).GET().build();
return this.httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()).thenApply(response -> {
if (response.statusCode() != 200) {
throw new CompletionException(new APIConnectionError("Could not fetch server public key!"));
}
return response.body();
}).thenApply(body -> {
if (!PEMConverter.validatePEM(body)) {
throw new CompletionException(new InvalidKeyException("PEM key had invalid format"));
}
return body;
});
}
/**
@ -249,8 +284,77 @@ public class SecureCompletionClient {
/**
* Core hybrid encryption: AES-256-GCM encrypts {@code payloadBytes} with {@code aesKey}.
*/
public CompletableFuture<byte[]> doEncrypt(byte[] payloadBytes, byte[] aesKey) {
throw new UnsupportedOperationException("Not yet implemented");
public CompletableFuture<byte[]> doEncrypt(byte[] payloadBytes, Key aesKey) {
return CompletableFuture.supplyAsync(() -> {
SecureRandom random = new SecureRandom();
byte[] nonce = new byte[12];
random.nextBytes(nonce);
Cipher cipher = null;
try {
cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(aesKey.getEncoded(), "AES"), new GCMParameterSpec(128, nonce));
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException |
InvalidKeyException e) {
throw new RuntimeException(e);
}
byte[] ciphertext;
try {
ciphertext = cipher.doFinal(payloadBytes);
} catch (IllegalBlockSizeException | BadPaddingException e) {
throw new RuntimeException(e);
}
String serverPEM;
try {
serverPEM = fetchServerPublicKey().get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(PEMConverter.fromPEM(serverPEM).getBytes());
PublicKey serverPublicKey;
try {
serverPublicKey = KeyFactory.getInstance("RSA").generatePublic(keySpec);
} catch (InvalidKeySpecException | NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
Cipher rsa;
byte[] enryptedAESKey = aesKey.getEncoded();
try {
rsa = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
rsa.init(Cipher.ENCRYPT_MODE, serverPublicKey);
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException e) {
throw new RuntimeException(e);
}
try {
rsa.doFinal(enryptedAESKey);
} catch (IllegalBlockSizeException | BadPaddingException e) {
throw new RuntimeException(e);
}
byte[] tag = Arrays.copyOfRange(ciphertext, ciphertext.length - (128 / Byte.SIZE), ciphertext.length);
EncryptedRequest request = new EncryptedRequest();
request.setVersion("1.0");
request.setAlgorithm("hybrid-aes256-rsa4096");
request.setEncryptedPayload(new EncryptedRequest.EncryptedPayload(Base64.getEncoder().encodeToString(ciphertext), Base64.getEncoder().encodeToString(nonce), Base64.getEncoder().encodeToString(tag)));
request.setEncryptedAESKey(Base64.getEncoder().encodeToString(enryptedAESKey));
request.setKeyAlgorithm("RSA-OAEP-SHA256");
request.setPayloadAlgorithm("AES-256-GCM");
return request.toJson().getBytes(StandardCharsets.UTF_8);
});
}
/**
@ -311,11 +415,7 @@ public class SecureCompletionClient {
keyInitLock.lock();
try {
if (keysInitialized) return;
if (keyDir == null || keyDir.isEmpty()) {
generateKeys(false);
} else {
generateKeys(true);
}
generateKeys(keyDir != null && !keyDir.isEmpty());
keysInitialized = true;
} finally {
keyInitLock.unlock();
@ -340,10 +440,10 @@ public class SecureCompletionClient {
try {
var kf = KeyFactory.getInstance("RSA");
try {
var crtSpec = kf.getKeySpec(key, java.security.spec.RSAPrivateCrtKeySpec.class);
var crtSpec = kf.getKeySpec(key, RSAPrivateCrtKeySpec.class);
return crtSpec.getModulus().bitLength();
} catch (Exception ignored) {
var privSpec = kf.getKeySpec(key, java.security.spec.RSAPrivateKeySpec.class);
var privSpec = kf.getKeySpec(key, RSAPrivateKeySpec.class);
return privSpec.getModulus().bitLength();
}
} catch (Exception ignored) {
@ -380,7 +480,7 @@ public class SecureCompletionClient {
* URL-encodes PEM key for {@code X-Public-Key} header.
*/
public String urlEncodePublicKey(String pemKey) {
return java.net.URLEncoder.encode(pemKey, StandardCharsets.UTF_8);
return URLEncoder.encode(pemKey, StandardCharsets.UTF_8);
}
/**