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

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 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"));
if (!resource.exists()) {
throw new IllegalStateException("No trust store for license server!");
}
int responseCode = 0;
try {
final var message = properties.getProperty("index") + ":" + Instant.now()
.toEpochMilli() * 1_000_000;
final var encryptedData = KeyUtils.encryptDataWithAESGCM(message, KeyUtils.generateSharedSecret(
KeyUtils.stringToPrivateKey(env.getProperty("my-private-key")),
KeyUtils.stringToPublicKey(env.getProperty("license-server-public-key"))));
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);
final var licenseOneShot = new LicenseOneShot(env.getProperty("my-folder"), env.getProperty("my-id"),
env.getProperty("my-private-key"), env.getProperty("license-server-public-key"),
env.getProperty("license-server-endpoint"),
resourceLoader.getResource(env.getProperty("license-server-trust")).getInputStream(),
env.getProperty("license-server-trust-password"));
licenseOneShot.check();
} catch (IOException 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;
import java.math.BigInteger;
import java.time.Instant;
@ -11,22 +12,22 @@ public class DeterministicHexSequenceWithTimestamp {
x = x.xor(x.shiftLeft(5)); // XOR shift and addition
x = x.add(x.shiftRight(3)); // Addition
x = x.multiply(BigInteger.valueOf(2654435761L))
.and(BigInteger.valueOf(0xFFFFFFFFFFFFFFFFL)); // Multiply by
// golden ratio,
// keep within 64
// bits
.and(BigInteger.valueOf(0xFFFFFFFFFFFFFFFFL)); // Multiply by
// golden ratio,
// keep within 64
// bits
x = x.xor(x.shiftRight(11))
.and(BigInteger.valueOf(0xFFFFFFFFFFFFFFFFL)); // More scrambling
.and(BigInteger.valueOf(0xFFFFFFFFFFFFFFFFL)); // More scrambling
// Mix timestamp into the transformation
BigInteger t_part = BigInteger.valueOf(timestamp)
.xor(BigInteger.valueOf(timestamp)
.shiftRight(32)); // Reduce
// to 64
// bits
.xor(BigInteger.valueOf(timestamp)
.shiftRight(32)); // Reduce
// to 64
// bits
x = x.add(t_part)
.xor(x.shiftLeft(7)
.or(t_part.shiftRight(3))); // More bit-mixing
.xor(x.shiftLeft(7)
.or(t_part.shiftRight(3))); // More bit-mixing
return x.and(BigInteger.valueOf(0xFFFFFFFFFFFFFFFFL)); // Keep within
// 64-bit range
}
@ -34,19 +35,26 @@ public class DeterministicHexSequenceWithTimestamp {
// Calculate the next value based on prev (hex string), f(prev, timestamp),
// and
// the fixed bit-length mask (hardcoded here)
private static BigInteger nextValue(String prevHex,
public static BigInteger nextValue(String prevHex,
long timestamp) {
// Convert the prev hex string to BigInteger
BigInteger prev = new BigInteger(prevHex, 16);
// Mask for a fixed bit length (e.g., 64 bits)
BigInteger mask = BigInteger.valueOf(1)
.shiftLeft(64)
.subtract(BigInteger.ONE); // 64-bit mask
.shiftLeft(64)
.subtract(BigInteger.ONE); // 64-bit mask
// Calculate next value using prev and timestamp, then apply mask
return prev.add(f(prev, timestamp))
.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,
@ -59,7 +67,7 @@ public class DeterministicHexSequenceWithTimestamp {
// Use nextValue method to get the next value
BigInteger next = nextValue(prevHex, timestamp);
prevHex = next.toString(16)
.toUpperCase(); // Convert next value to hex string
.toUpperCase(); // Convert next value to hex string
// Print next value in BigInteger and its fixed-length hexadecimal
// representation
@ -73,7 +81,7 @@ public class DeterministicHexSequenceWithTimestamp {
String startHex = "1A3F"; // Initial seed value in hex
int sequenceLength = 10; // Number of elements
long requestTimestamp = Instant.now()
.toEpochMilli() * 1_000_000; // Nanoseconds timestamp
.toEpochMilli() * 1_000_000; // Nanoseconds timestamp
int fixedBitLength = 64; // Fixed length of 64 bits for BigInteger and
// hex output

View file

@ -1,8 +1,11 @@
package com.example.app.utils;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
@ -18,7 +21,7 @@ import javax.net.ssl.TrustManagerFactory;
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 {
KeyStore trustStore = KeyStore.getInstance("JKS");
@ -53,11 +56,16 @@ public final class LicenseUtils {
// Read response
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
connection.disconnect();
return responseCode;
return responseBody;
} catch (MalformedURLException e) {
throw new RuntimeException("Malformed URL!", 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();
}
}
}