Start with encryption
This commit is contained in:
parent
9df61e0cd3
commit
b6af1c9792
8 changed files with 413 additions and 79 deletions
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue