server side checks
This commit is contained in:
parent
8d227b26d3
commit
661e783f24
8 changed files with 255 additions and 89 deletions
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"java.compile.nullAnalysis.mode": "disabled"
|
||||
}
|
||||
135
app/src/main/java/com/example/app/LicenseOneShot.java
Normal file
135
app/src/main/java/com/example/app/LicenseOneShot.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,12 @@ import java.io.FileInputStream;
|
|||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
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.Properties;
|
||||
|
||||
|
|
@ -34,7 +40,7 @@ public class LicenseRest {
|
|||
String[] split = payload.split("\\.");
|
||||
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 encrypted = split[1];
|
||||
final var signature = split[2];
|
||||
|
|
@ -58,33 +64,55 @@ public class LicenseRest {
|
|||
KeyUtils.stringToPublicKey(props.getProperty("sender-public-key"))));
|
||||
System.out.println("Decrypted: " + decrypted);
|
||||
|
||||
// TODO: verify signature
|
||||
// verify signature
|
||||
boolean isVerified = KeyUtils.verifySignature(decrypted, signature,
|
||||
KeyUtils.stringToPublicKey(props.getProperty("sender-public-key")));
|
||||
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 currentSenderIndex = decryptedSplit[0];
|
||||
final var timestamp = Long.valueOf(decryptedSplit[1]);
|
||||
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
|
||||
.nextValueString(props.getProperty("index"), timestamp);
|
||||
System.out.println("Current sender index: " + currentSenderIndex + " timestamp = " + timestamp
|
||||
+ " current local index = " + currentLocalIndex + " next index = " + nextLocalIndex);
|
||||
|
||||
// TODO: veryfy current index match, increment index using timestamp then send
|
||||
// new index in response.
|
||||
setExpiration(Instant.ofEpochMilli(timestamp));
|
||||
|
||||
// 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.getProperty("index"));
|
||||
if (!StringUtils.hasText(props.getProperty("index"))) {
|
||||
props.setProperty("index", "1A3F");
|
||||
saveProperties(filePath, props);
|
||||
} else {
|
||||
props.setProperty("index", DeterministicHexSequenceWithTimestamp
|
||||
.nextValueString(props.getProperty("index"), 0));
|
||||
props.setProperty("index", nextLocalIndex);
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,5 +4,6 @@ server.ssl.key-store=classpath:server.jks
|
|||
server.ssl.key-store-password=test
|
||||
server.ssl.key-store-type=JKS
|
||||
server.ssl.key-alias=torsim-license-server
|
||||
#server.ssl.key-alias=torsim-license-server-dummy
|
||||
|
||||
license-folder=${user.home}/test/license/server
|
||||
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue