server side checks

This commit is contained in:
petre.rosioru 2025-03-11 17:25:30 +02:00
parent 8d227b26d3
commit 661e783f24
8 changed files with 255 additions and 89 deletions

3
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,3 @@
{
"java.compile.nullAnalysis.mode": "disabled"
}

View file

@ -0,0 +1,135 @@
package com.example.app;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.time.Instant;
import java.util.Base64;
import java.util.Properties;
import com.example.app.utils.DeterministicHexSequenceWithTimestamp;
import com.example.app.utils.KeyUtils;
import com.example.app.utils.LicenseUtils;
public class LicenseOneShot {
private final String myId;
private final String myPrivateKey;
private final String licenseServerPublicKey;
private final String licenseServerEndpoint;
private final InputStream trustStoreIO;
private final String trustStorePassword;
private final String filePath;
private final Properties properties = new Properties();
public LicenseOneShot(String myFolder, String myId, String myPrivateKey,
String licenseServerPublicKey, String licenseServerEndpoint, InputStream trustStoreIO,
String trustStorePassword) {
this.myId = myId;
this.myPrivateKey = myPrivateKey;
this.licenseServerPublicKey = licenseServerPublicKey;
this.licenseServerEndpoint = licenseServerEndpoint;
this.trustStoreIO = trustStoreIO;
this.trustStorePassword = trustStorePassword;
this.filePath = myFolder + File.separator + myId;
checkProperties(this.properties, filePath);
}
public void check() {
// TODO: check expiration...
final var requestTimestamp = Instant.now()
.toEpochMilli();
final var message = properties.getProperty("index") + ":" + requestTimestamp;
final var encryptedData = KeyUtils.encryptDataWithAESGCM(message, KeyUtils.generateSharedSecret(
KeyUtils.stringToPrivateKey(myPrivateKey),
KeyUtils.stringToPublicKey(licenseServerPublicKey)));
final var encryptedRequest = Base64.getUrlEncoder()
.encodeToString(encryptedData);
final var signatureRequest = KeyUtils.signMessage(message,
KeyUtils.stringToPrivateKey(myPrivateKey));
final var responseBody = LicenseUtils
.request(licenseServerEndpoint, trustStoreIO,
trustStorePassword,
Base64.getUrlEncoder()
.encodeToString(myId.getBytes()) + "." + encryptedRequest + "."
+ signatureRequest);
// decrypt response
final var splitResponse = responseBody.split("\\.");
final var encryptedResponse = splitResponse[0];
final var signatureResponse = splitResponse[1];
final var decryptedResponse = KeyUtils
.decryptDataWithAESGCM(Base64.getUrlDecoder().decode(encryptedResponse),
KeyUtils.generateSharedSecret(
KeyUtils.stringToPrivateKey(myPrivateKey),
KeyUtils.stringToPublicKey(licenseServerPublicKey)));
// verify signature of response
final var isVerified = KeyUtils.verifySignature(decryptedResponse, signatureResponse,
KeyUtils.stringToPublicKey(licenseServerPublicKey));
System.out.println("Is verified? " + isVerified);
if (!isVerified) {
throw new IllegalStateException("License not valid! Invalid signature.");
}
// parse response
final var splitDecryptedResponse = decryptedResponse.split("\\:");
// check values and throw if license is bad
System.out.println("License server index: " + splitDecryptedResponse[0]);
final var nextLocalIndex = DeterministicHexSequenceWithTimestamp
.nextValueString(properties.getProperty("index"), requestTimestamp);
System.out.println("Calculated local index: " + nextLocalIndex);
if (!splitDecryptedResponse[0].equals(nextLocalIndex)) {
throw new IllegalStateException("License not valid!");
}
// persist license on property file.
properties.setProperty("index", splitDecryptedResponse[0]);
properties.setProperty("until", splitDecryptedResponse[1]);
saveProperties(properties, this.filePath);
}
private void checkProperties(Properties properties,
String filePath) {
try {
File file = new File(filePath);
if (!file.exists()) {
createDefaultProperties(properties, filePath);
} else {
loadProperties(properties, filePath);
}
} catch (IOException e) {
throw new RuntimeException("IO Exception!", e);
}
}
private void createDefaultProperties(Properties properties,
String filePath) throws IOException {
properties.setProperty("index", "1A3F");
saveProperties(properties, filePath);
}
private void saveProperties(Properties properties,
String filePath) {
try (FileOutputStream fos = new FileOutputStream(filePath)) {
properties.store(fos, "Updated properties file");
} catch (IOException e) {
throw new RuntimeException("IO Exception!", e);
}
}
private void loadProperties(Properties properties,
String filePath) throws IOException {
try (FileInputStream fis = new FileInputStream(filePath)) {
properties.load(fis);
}
}
}

View file

@ -32,73 +32,20 @@ public class StartupValidator implements ApplicationContextInitializer<Configura
final ResourceLoader resourceLoader = context; final ResourceLoader resourceLoader = context;
final var env = context.getEnvironment(); final var env = context.getEnvironment();
final var filePath = env.getProperty("my-folder") + File.separator + env.getProperty("my-id");
final var properties = new Properties();
checkProperties(properties, filePath);
final var resource = resourceLoader.getResource(env.getProperty("license-server-trust")); final var resource = resourceLoader.getResource(env.getProperty("license-server-trust"));
if (!resource.exists()) { if (!resource.exists()) {
throw new IllegalStateException("No trust store for license server!"); throw new IllegalStateException("No trust store for license server!");
} }
int responseCode = 0;
try { try {
final var licenseOneShot = new LicenseOneShot(env.getProperty("my-folder"), env.getProperty("my-id"),
final var message = properties.getProperty("index") + ":" + Instant.now() env.getProperty("my-private-key"), env.getProperty("license-server-public-key"),
.toEpochMilli() * 1_000_000; env.getProperty("license-server-endpoint"),
final var encryptedData = KeyUtils.encryptDataWithAESGCM(message, KeyUtils.generateSharedSecret( resourceLoader.getResource(env.getProperty("license-server-trust")).getInputStream(),
KeyUtils.stringToPrivateKey(env.getProperty("my-private-key")), env.getProperty("license-server-trust-password"));
KeyUtils.stringToPublicKey(env.getProperty("license-server-public-key")))); licenseOneShot.check();
final var encrypted = Base64.getUrlEncoder()
.encodeToString(encryptedData);
final var signature = KeyUtils.signMessage(message,
KeyUtils.stringToPrivateKey(env.getProperty("my-private-key")));
responseCode = LicenseUtils.request(env.getProperty("license-server-endpoint"), resource.getInputStream(),
env.getProperty("license-server-trust-password"),
Base64.getUrlEncoder()
.encodeToString(env.getProperty("my-id").getBytes()) + "." + encrypted + "." + signature);
} catch (IOException e) { } catch (IOException e) {
throw new IllegalStateException("IO exception for trust store!", e); throw new IllegalStateException("IO exception for trust store!", e);
} }
if (responseCode != 200) {
throw new IllegalStateException("License not valid!");
}
}
private void checkProperties(Properties properties,
String filePath) {
try {
File file = new File(filePath);
if (!file.exists()) {
createDefaultProperties(properties, filePath);
} else {
loadProperties(properties, filePath);
}
} catch (IOException e) {
throw new RuntimeException("IO Exception!", e);
}
}
private void createDefaultProperties(Properties properties,
String filePath) throws IOException {
properties.setProperty("index", "");
saveProperties(properties, filePath);
}
private void saveProperties(Properties properties,
String filePath) throws IOException {
try (FileOutputStream fos = new FileOutputStream(filePath)) {
properties.store(fos, "Updated properties file");
}
}
private void loadProperties(Properties properties,
String filePath) throws IOException {
try (FileInputStream fis = new FileInputStream(filePath)) {
properties.load(fis);
}
} }
} }

View file

@ -1,5 +1,6 @@
package com.example.app.utils; package com.example.app.utils;
import java.math.BigInteger; import java.math.BigInteger;
import java.time.Instant; import java.time.Instant;
@ -34,7 +35,7 @@ public class DeterministicHexSequenceWithTimestamp {
// Calculate the next value based on prev (hex string), f(prev, timestamp), // Calculate the next value based on prev (hex string), f(prev, timestamp),
// and // and
// the fixed bit-length mask (hardcoded here) // the fixed bit-length mask (hardcoded here)
private static BigInteger nextValue(String prevHex, public static BigInteger nextValue(String prevHex,
long timestamp) { long timestamp) {
// Convert the prev hex string to BigInteger // Convert the prev hex string to BigInteger
BigInteger prev = new BigInteger(prevHex, 16); BigInteger prev = new BigInteger(prevHex, 16);
@ -49,6 +50,13 @@ public class DeterministicHexSequenceWithTimestamp {
.and(mask); // Calculate next and apply mask .and(mask); // Calculate next and apply mask
} }
public static String nextValueString(String prevHex,
long timestamp) {
BigInteger next = nextValue(prevHex, timestamp);
return next.toString(16)
.toUpperCase();
}
public static void generateSequence(String startHex, public static void generateSequence(String startHex,
int length, int length,
long timestamp, long timestamp,

View file

@ -1,8 +1,11 @@
package com.example.app.utils; package com.example.app.utils;
import java.io.BufferedReader;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
@ -18,7 +21,7 @@ import javax.net.ssl.TrustManagerFactory;
public final class LicenseUtils { public final class LicenseUtils {
public static int request(String endpoint, InputStream trustInput, String trustPassword, String payload) { public static String request(String endpoint, InputStream trustInput, String trustPassword, String payload) {
try { try {
KeyStore trustStore = KeyStore.getInstance("JKS"); KeyStore trustStore = KeyStore.getInstance("JKS");
@ -53,11 +56,16 @@ public final class LicenseUtils {
// Read response // Read response
int responseCode = connection.getResponseCode(); int responseCode = connection.getResponseCode();
System.out.println("Response Code: " + responseCode); if(responseCode != 200){
throw new RuntimeException("License server error! HTTP STATUS CODE: " + responseCode);
}
String responseBody = readResponse(connection);
System.out.println("Response Body: " + responseBody);
// Close connection // Close connection
connection.disconnect(); connection.disconnect();
return responseCode; return responseBody;
} catch (MalformedURLException e) { } catch (MalformedURLException e) {
throw new RuntimeException("Malformed URL!", e); throw new RuntimeException("Malformed URL!", e);
} catch (IOException e) { } catch (IOException e) {
@ -73,4 +81,23 @@ public final class LicenseUtils {
} }
} }
private static String readResponse(HttpURLConnection connection) throws IOException {
InputStream inputStream = (connection.getResponseCode() >= 400)
? connection.getErrorStream() // Handle errors
: connection.getInputStream();
if (inputStream == null) {
return "No response";
}
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
response.append(line).append("\n");
}
return response.toString().trim();
}
}
} }

View file

@ -5,6 +5,12 @@ import java.io.FileInputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.Base64; import java.util.Base64;
import java.util.Properties; import java.util.Properties;
@ -34,7 +40,7 @@ public class LicenseRest {
String[] split = payload.split("\\."); String[] split = payload.split("\\.");
File directory = new File(licenseDirectory); File directory = new File(licenseDirectory);
// TODO: extract ID, encrypted message and signature // extract ID, encrypted message and signature
final var id = new String(Base64.getUrlDecoder().decode(split[0]), StandardCharsets.UTF_8); final var id = new String(Base64.getUrlDecoder().decode(split[0]), StandardCharsets.UTF_8);
final var encrypted = split[1]; final var encrypted = split[1];
final var signature = split[2]; final var signature = split[2];
@ -58,33 +64,55 @@ public class LicenseRest {
KeyUtils.stringToPublicKey(props.getProperty("sender-public-key")))); KeyUtils.stringToPublicKey(props.getProperty("sender-public-key"))));
System.out.println("Decrypted: " + decrypted); System.out.println("Decrypted: " + decrypted);
// TODO: verify signature // verify signature
boolean isVerified = KeyUtils.verifySignature(decrypted, signature, boolean isVerified = KeyUtils.verifySignature(decrypted, signature,
KeyUtils.stringToPublicKey(props.getProperty("sender-public-key"))); KeyUtils.stringToPublicKey(props.getProperty("sender-public-key")));
System.out.println("Is verified? " + isVerified); System.out.println("Is verified? " + isVerified);
// TODO: parse data if (!isVerified) {
throw new RuntimeException("Invalid signature!");
}
// parse data
final var decryptedSplit = decrypted.split("\\:"); final var decryptedSplit = decrypted.split("\\:");
final var currentSenderIndex = decryptedSplit[0]; final var currentSenderIndex = decryptedSplit[0];
final var timestamp = Long.valueOf(decryptedSplit[1]); final var timestamp = Long.valueOf(decryptedSplit[1]);
final var currentLocalIndex = props.getProperty("index"); final var currentLocalIndex = props.getProperty("index");
// TODO: check current local index if it matches with current sender index
// check current local index if it matches with current sender index
final var nextLocalIndex = DeterministicHexSequenceWithTimestamp final var nextLocalIndex = DeterministicHexSequenceWithTimestamp
.nextValueString(props.getProperty("index"), timestamp); .nextValueString(props.getProperty("index"), timestamp);
System.out.println("Current sender index: " + currentSenderIndex + " timestamp = " + timestamp System.out.println("Current sender index: " + currentSenderIndex + " timestamp = " + timestamp
+ " current local index = " + currentLocalIndex + " next index = " + nextLocalIndex); + " current local index = " + currentLocalIndex + " next index = " + nextLocalIndex);
// TODO: veryfy current index match, increment index using timestamp then send setExpiration(Instant.ofEpochMilli(timestamp));
// new index in response.
// veryfy current index match, increment index using timestamp then send new
// index in response.
if (!currentSenderIndex.equals(currentLocalIndex)) {
throw new RuntimeException("Invalid current index!");
}
System.out.println("Props: " + props); System.out.println("Props: " + props);
System.out.println(props.getProperty("index"));
if (!StringUtils.hasText(props.getProperty("index"))) { if (!StringUtils.hasText(props.getProperty("index"))) {
props.setProperty("index", "1A3F"); props.setProperty("index", "1A3F");
saveProperties(filePath, props); saveProperties(filePath, props);
} else { } else {
props.setProperty("index", DeterministicHexSequenceWithTimestamp props.setProperty("index", nextLocalIndex);
.nextValueString(props.getProperty("index"), 0)); saveProperties(filePath, props);
final var messageResponse = nextLocalIndex + ":"
+ setExpiration(Instant.ofEpochMilli(timestamp)).toEpochMilli();
final var encryptedDataResponse = KeyUtils.encryptDataWithAESGCM(messageResponse,
KeyUtils.generateSharedSecret(
KeyUtils.stringToPrivateKey(props.getProperty("receiver-private-key")),
KeyUtils.stringToPublicKey(props.getProperty("sender-public-key"))));
final var encryptedResponse = Base64.getUrlEncoder()
.encodeToString(encryptedDataResponse);
final var signatureResponse = KeyUtils.signMessage(messageResponse,
KeyUtils.stringToPrivateKey(props.getProperty("receiver-private-key")));
return encryptedResponse + "." + signatureResponse;
} }
} }
} }
} }
@ -108,4 +136,21 @@ public class LicenseRest {
} }
} }
public Instant setExpiration(Instant now) {
// Convert Instant to LocalDate in a specific time zone
ZoneId zoneId = ZoneId.systemDefault(); // Change if needed
LocalDate localDate = now.atZone(zoneId).toLocalDate();
// Set the time to 23:59:59.999999999
LocalTime endOfDay = LocalTime.MAX; // Equivalent to 23:59:59.999999999
ZonedDateTime endOfDayZoned = ZonedDateTime.of(localDate, endOfDay, ZoneOffset.UTC);
System.out.println("Now: " + now);
System.out.println("End of Day: " + endOfDayZoned);
// Convert back to Instant
return endOfDayZoned.toInstant();
}
} }

View file

@ -4,5 +4,6 @@ server.ssl.key-store=classpath:server.jks
server.ssl.key-store-password=test server.ssl.key-store-password=test
server.ssl.key-store-type=JKS server.ssl.key-store-type=JKS
server.ssl.key-alias=torsim-license-server server.ssl.key-alias=torsim-license-server
#server.ssl.key-alias=torsim-license-server-dummy
license-folder=${user.home}/test/license/server license-folder=${user.home}/test/license/server