Commit fe29e8d1 authored by Cypres TAC's avatar Cypres TAC
Browse files

Merge branch 'feat/instants' into 'master'

Feat/instants

See merge request !19
parents 7074c528 3ec91421
Pipeline #231612 passed with stages
in 3 minutes and 19 seconds
......@@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>fr.inria.clea</groupId>
<artifactId>clea-crypto</artifactId>
<version>0.0.1-SNAPSHOT</version>
<version>0.0.2-SNAPSHOT</version>
<dependencies>
<dependency>
......@@ -22,11 +22,23 @@
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.68</version>
</dependency>
<!-- Bean validation -->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.13.Final</version>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.el</artifactId>
<version>3.0.0</version>
</dependency>
<!-- end Bean validation -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
......
......@@ -20,6 +20,7 @@ import java.security.SecureRandom;
import java.security.Security;
import java.security.spec.ECGenParameterSpec;
import java.security.spec.InvalidKeySpecException;
import java.time.Instant;
import java.util.Arrays;
import java.util.UUID;
......@@ -44,6 +45,10 @@ import org.bouncycastle.jce.spec.ECPublicKeySpec;
import org.bouncycastle.math.ec.ECPoint;
import org.bouncycastle.util.encoders.Hex;
import fr.inria.clea.lsp.exception.CleaEncryptionException;
import fr.inria.clea.lsp.utils.TimeUtils;
import lombok.extern.slf4j.Slf4j;
/**
* Encryption/Decription respecting ECIES-KEM (Elliptic Curve Integrated
* Encryption Scheme with Key encapsulation mechanisms )
......@@ -56,6 +61,7 @@ import org.bouncycastle.util.encoders.Hex;
* for Public-Key Encryption”, 2006</a>
*
*/
@Slf4j
public class CleaEciesEncoder {
/* Type of the elliptic curve */
......@@ -249,65 +255,6 @@ public class CleaEciesEncoder {
return out;
}
/**
* concat two bytes array in one
*
* @param part1 first bytes array
* @param part2 second bytes array
* @return bytes array concatenation
*/
protected byte[] concat(byte[] part1, byte[] part2) throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
outputStream.write(part1);
outputStream.write(part2);
byte concatParts[] = outputStream.toByteArray();
return concatParts;
}
/**
* convert 32 bytes array in UUID format
*
* @param bytes 32 bytes array
* @return UUID
*/
protected UUID bytesToUuid(byte[] bytes) {
ByteBuffer bb = ByteBuffer.wrap(bytes);
long firstLong = bb.getLong();
long secondLong = bb.getLong();
return new UUID(firstLong, secondLong);
}
/**
* convert a UUID format in 32 bytes array
*
* @param uuid UUID
* @return 32 bytes array
*/
protected byte[] uuidToBytes(UUID uuid) {
ByteBuffer buffer = ByteBuffer.wrap(new byte[16]);
buffer.putLong(uuid.getMostSignificantBits());
buffer.putLong(uuid.getLeastSignificantBits());
return buffer.array();
}
/**
* convert an int in 32 bytes array
*
* @param data int to be converted
* @return 32 bytes array
*/
private static byte[] intToBytes(int data) {
return new byte[] {
(byte) ((data >> 24) & 0xff),
(byte) ((data >> 16) & 0xff),
(byte) ((data >> 8) & 0xff),
(byte) ((data >> 0) & 0xff) };
}
/**
* Compute the LTKey (Temporary Location Key) respecting Cléa protocol
* LTKey(t_periodStart) = SHA256(SK_L | t_periodStart)
......@@ -318,9 +265,10 @@ public class CleaEciesEncoder {
* @return LTKey (Temporary Location Key)
* @throws CleaEncryptionException
*/
public byte[] computeLocationTemporarySecretKey(String permanentLocationSecretKey, int periodStartTime) throws CleaEncryptionException {
public byte[] computeLocationTemporarySecretKey(String permanentLocationSecretKey, Instant periodStartTime) throws CleaEncryptionException {
log.info("permanentLocationSecretKey: {}, periodStartTime= {}", permanentLocationSecretKey, periodStartTime);
try {
byte[] concatKey = this.concat(intToBytes(periodStartTime), Hex.decode(permanentLocationSecretKey));
byte[] concatKey = this.concat(instantToBytes(periodStartTime), Hex.decode(permanentLocationSecretKey));
MessageDigest msg = MessageDigest.getInstance("SHA-256");
byte[] locationTemporarySecretKey = msg.digest(concatKey);
......@@ -359,4 +307,68 @@ public class CleaEciesEncoder {
}
}
private byte[] instantToBytes(Instant periodStartTime) {
long timestamp = TimeUtils.ntpTimestampFromInstant(periodStartTime);
return intToBytes((int) timestamp);
}
/**
* convert an int in 32 bytes array
*
* @param data int to be converted
* @return 32 bytes array
*/
private static byte[] intToBytes(int data) {
return new byte[] {
(byte) ((data >> 24) & 0xff),
(byte) ((data >> 16) & 0xff),
(byte) ((data >> 8) & 0xff),
(byte) ((data >> 0) & 0xff) };
}
/**
* concat two bytes array in one
*
* @param part1 first bytes array
* @param part2 second bytes array
* @return bytes array concatenation
*/
protected byte[] concat(byte[] part1, byte[] part2) throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
outputStream.write(part1);
outputStream.write(part2);
byte concatParts[] = outputStream.toByteArray();
return concatParts;
}
/**
* convert 32 bytes array in UUID format
*
* @param bytes 32 bytes array
* @return UUID
*/
protected UUID bytesToUuid(byte[] bytes) {
ByteBuffer bb = ByteBuffer.wrap(bytes);
long firstLong = bb.getLong();
long secondLong = bb.getLong();
return new UUID(firstLong, secondLong);
}
/**
* convert a UUID format in 32 bytes array
*
* @param uuid UUID
* @return 32 bytes array
*/
protected byte[] uuidToBytes(UUID uuid) {
ByteBuffer buffer = ByteBuffer.wrap(new byte[16]);
buffer.putLong(uuid.getMostSignificantBits());
buffer.putLong(uuid.getLeastSignificantBits());
return buffer.array();
}
}
......@@ -9,6 +9,8 @@ import javax.validation.constraints.Max;
import org.bouncycastle.util.Arrays;
import fr.inria.clea.lsp.exception.CleaEncodingException;
import fr.inria.clea.lsp.exception.CleaEncryptionException;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
......
package fr.inria.clea.lsp;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Base64;
import java.util.Objects;
import java.util.UUID;
import fr.devnied.bitlib.BytesUtils;
import fr.inria.clea.lsp.utils.TimeUtils;
import fr.inria.clea.lsp.exception.CleaCryptoException;
import fr.inria.clea.lsp.exception.CleaEncryptionException;
import lombok.Builder;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
......@@ -31,8 +34,8 @@ public class Location {
* @return the deep link as a String
* @throws CleaEncryptionException
*/
public String newDeepLink() throws CleaEncryptionException {
int periodStartTime = TimeUtils.hourRoundedCurrentTimeTimestamp32();
public String newDeepLink() throws CleaCryptoException {
Instant periodStartTime = Instant.now().truncatedTo(ChronoUnit.HOURS);
return this.newDeepLink(periodStartTime);
}
......@@ -41,14 +44,11 @@ public class Location {
* at the given period start time with a QR code validaty
* starting at the period start time.
*
* @param periodStartTime Starting time of the period, expressed as the number
* of seconds since January 1st, 1900 (NTP timestamp limited to the 32-bit
* seconds field), by convention in the UTC (Coordinated Universal Time) timezone.
* A period necessarily starts at a round hour.
* @param periodStartTime Starting time of the period. A period necessarily starts at a round hour.
* @return the deep link as a String
* @throws CleaEncryptionException
*/
public String newDeepLink(int periodStartTime) throws CleaEncryptionException {
public String newDeepLink(Instant periodStartTime) throws CleaCryptoException {
// QR-code validity starts at period start time
return this.newDeepLink(periodStartTime, periodStartTime);
}
......@@ -58,18 +58,12 @@ public class Location {
* at the given period start time with a QR code validaty
* starting at the period start time.
*
* @param periodStartTime Starting time of the period, expressed as the number
* of seconds since January 1st, 1900 (NTP timestamp limited to the 32-bit
* seconds field), by convention in the UTC (Coordinated Universal Time) timezone.
* A period necessarily starts at a round hour.
* @param qrCodeValidityStartTime Starting time of the QR code validity timespan,
* expressed as the number of seconds since January 1st, 1900 (NTP timestamp
* limited to the 32-bit seconds field),
* by convention in UTC (Coordinated Universal Time) timezone.
* @param periodStartTime Starting time of the period. A period necessarily starts at a round hour.
* @param qrCodeValidityStartTime Starting time of the QR code validity timespan.
* @return the deep link as a String
* @throws CleaEncryptionException
*/
public String newDeepLink(int periodStartTime, int qrCodeValidityStartTime) throws CleaEncryptionException {
public String newDeepLink(Instant periodStartTime, Instant qrCodeValidityStartTime) throws CleaCryptoException {
this.setPeriodStartTime(periodStartTime);
this.setQrCodeValidityStartTime(periodStartTime, qrCodeValidityStartTime);
return COUNTRY_SPECIFIC_PREFIX + this.getLocationSpecificPartEncryptedBase64();
......@@ -80,22 +74,22 @@ public class Location {
* @return the base 64 encoded location specific part
* @throws CleaEncryptionException
*/
public String getLocationSpecificPartEncryptedBase64() throws CleaEncryptionException {
public String getLocationSpecificPartEncryptedBase64() throws CleaCryptoException {
return Base64.getEncoder().encodeToString(this.getLocationSpecificPartEncrypted());
}
protected byte[] getLocationSpecificPartEncrypted() throws CleaEncryptionException {
protected byte[] getLocationSpecificPartEncrypted() throws CleaCryptoException {
if (Objects.nonNull(this.contact)) {
this.locationSpecificPart.setEncryptedLocationContactMessage(this.getLocationContactMessageEncrypted());
}
return new LocationSpecificPartEncoder(this.serverAuthorityPublicKey).encode(locationSpecificPart);
}
protected byte[] getLocationContactMessageEncrypted() throws CleaEncryptionException {
protected byte[] getLocationContactMessageEncrypted() throws CleaCryptoException {
return new LocationContactMessageEncoder(this.manualContactTracingAuthorityPublicKey).encode(contact);
}
protected void setPeriodStartTime(int periodStartTime) throws CleaEncryptionException {
protected void setPeriodStartTime(Instant periodStartTime) throws CleaEncryptionException {
byte[] locationTemporarySecretKey = this.getCleaEncoder().computeLocationTemporarySecretKey(this.permanentLocationSecretKey, periodStartTime);
UUID currentLocationTemporaryPublicId = this.getCleaEncoder().computeLocationTemporaryPublicId(locationTemporarySecretKey);
this.locationSpecificPart.setPeriodStartTime(periodStartTime);
......@@ -104,32 +98,32 @@ public class Location {
if (Objects.nonNull(this.contact)) {
this.contact.setPeriodStartTime(periodStartTime);
}
log.debug("new periodStartTime: {} ", Integer.toUnsignedString(periodStartTime));
log.debug("new periodStartTime: {} ", periodStartTime);
log.debug("locationTemporarySecretKey*: {}*", BytesUtils.bytesToString(locationTemporarySecretKey));
log.debug("locationTemporaryPublicID: " + currentLocationTemporaryPublicId.toString());
}
protected void setQrCodeValidityStartTime(int periodStartTime, int qrCodeValidityStartTime) {
protected void setQrCodeValidityStartTime(Instant periodStartTime, Instant qrCodeValidityStartTime) {
if ((this.locationSpecificPart.getQrCodeRenewalInterval() == 0)
&& (this.locationSpecificPart.getQrCodeValidityStartTime() != 0)) {
&& Objects.nonNull(this.locationSpecificPart.getQrCodeValidityStartTime())) {
log.warn("Cannot update QrCode validity start time. No renewal specified!");
return;
}
if(qrCodeValidityStartTime < periodStartTime){
if (qrCodeValidityStartTime.isBefore(periodStartTime)) {
log.warn("Cannot set QrCode validity start time to {}. It preceeds period validity (start: {}, duration (in hours): {}",
qrCodeValidityStartTime, periodStartTime, this.locationSpecificPart.getPeriodDuration());
return;
}
if (qrCodeValidityStartTime > periodStartTime + this.locationSpecificPart.getPeriodDuration() * TimeUtils.NB_SECONDS_PER_HOUR) {
if (qrCodeValidityStartTime.isAfter(periodStartTime.plus(this.locationSpecificPart.getPeriodDuration(), ChronoUnit.HOURS))) {
log.warn("Cannot set QrCode validity start time to {}. It exceeds period validity (start: {}, duration (in hours): {}",
qrCodeValidityStartTime, periodStartTime, this.locationSpecificPart.getPeriodDuration());
return;
}
if ((this.locationSpecificPart.getQrCodeRenewalInterval() != 0) &&
(((qrCodeValidityStartTime - periodStartTime) % this.locationSpecificPart.getQrCodeRenewalInterval()) != 0)) {
(((qrCodeValidityStartTime.getEpochSecond() - periodStartTime.getEpochSecond()) % this.locationSpecificPart.getQrCodeRenewalInterval()) != 0)) {
log.warn("Cannot set QrCode validity start time to {}. It is not a multiple of qrCodeRenewalInterval (qrCodeValidityStartTime: {}, periodStartTime: {}, qrCodeRenewalInterval: {}",
qrCodeValidityStartTime, periodStartTime, this.locationSpecificPart.getQrCodeRenewalInterval());
return;
......
package fr.inria.clea.lsp;
import javax.validation.constraints.Max;
import java.time.Instant;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.AccessLevel;
@Builder
@Getter
@EqualsAndHashCode
@ToString
public class LocationContact {
public static final String PHONE_VALIDATION_MESSAGE = "Location phone is mandatory";
public static final String PIN_VALIDATION_MESSAGE = "Secret digit PIN must contain exactly 6 characters";
public static final String PERIOD_START_TIME_VALIDATION_MESSAGE = "Period start time must not be null";
/* Phone number of the location contact person, one digit = one character */
@NotBlank(message= PHONE_VALIDATION_MESSAGE)
String locationPhone;
/* Secret 6 digit PIN, one digit = one character */
@Max(value = 6)
// TODO: set max to 6 when CSV files used for tests are updated
@Size(min = 6, max = 8,
message = PIN_VALIDATION_MESSAGE)
String locationPin;
/* Starting time of the period in seconds */
@NotNull(message= PERIOD_START_TIME_VALIDATION_MESSAGE)
@Setter(AccessLevel.PROTECTED)
int periodStartTime;
Instant periodStartTime;
}
......@@ -5,24 +5,38 @@ import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.spec.InvalidKeySpecException;
import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import org.bouncycastle.crypto.InvalidCipherTextException;
import fr.devnied.bitlib.BitUtils;
import fr.inria.clea.lsp.exception.CleaCryptoException;
import fr.inria.clea.lsp.exception.CleaEncryptionException;
import fr.inria.clea.lsp.exception.CleaInvalidLocationContactMessageException;
import fr.inria.clea.lsp.utils.TimeUtils;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class LocationContactMessageEncoder {
private String manualContactTracingAuthorityPublicKey;
private CleaEciesEncoder cleaEncoder;
private Validator validator;
public LocationContactMessageEncoder(String manualContactTracingAuthorityPublicKey) {
super();
this.manualContactTracingAuthorityPublicKey = manualContactTracingAuthorityPublicKey;
cleaEncoder = new CleaEciesEncoder();
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
validator = factory.getValidator();
}
public byte[] encode(LocationContact message) throws CleaEncryptionException {
public byte[] encode(LocationContact message) throws CleaCryptoException {
this.validateMessage(message);
try {
byte[] messageBinary = this.getBinaryMessage(message);
byte[] encryptedLocationContactMessage = cleaEncoder.encrypt(null, messageBinary, manualContactTracingAuthorityPublicKey);
......@@ -36,6 +50,16 @@ public class LocationContactMessageEncoder {
throw new CleaEncryptionException(e);
}
}
protected void validateMessage(LocationContact message) throws CleaInvalidLocationContactMessageException {
Set<ConstraintViolation<LocationContact>> violations = validator.validate(message);
for (ConstraintViolation<LocationContact> violation : violations) {
log.error(violation.getMessage());
}
if (!violations.isEmpty()) {
throw new CleaInvalidLocationContactMessageException(violations);
}
}
/**
* Encode the data locContactMsg in binary format:
......@@ -67,7 +91,8 @@ public class LocationContactMessageEncoder {
}
/* t_periodStart (32 bits) */
locationContactMessage.setNextInteger(message.getPeriodStartTime(), 32);
long periodStartTime = TimeUtils.ntpTimestampFromInstant(message.getPeriodStartTime());
locationContactMessage.setNextLong(periodStartTime, 32);
return locationContactMessage.getData();
}
......@@ -77,7 +102,7 @@ public class LocationContactMessageEncoder {
* | locationPhone | locationPIN | t_periodStart |
* @throws CleaEncryptionException
*/
public LocationContact decode(byte[] encryptedLocationContactMessage) throws CleaEncryptionException {
public LocationContact decode(byte[] encryptedLocationContactMessage) throws CleaCryptoException {
try {
/* Decrypt the data */
byte[] binaryLocationContactMessage = cleaEncoder.decrypt(encryptedLocationContactMessage, this.manualContactTracingAuthorityPublicKey , false);
......@@ -105,9 +130,12 @@ public class LocationContactMessageEncoder {
}
/* t_periodStart (32 bits) */
int periodStartTime = bitLocationContactMessage.getNextInteger(32);
long periodStartTime = bitLocationContactMessage.getNextLong(32);
return new LocationContact(locationPhone.toString(), locationPin.toString(), periodStartTime);
LocationContact locationContact = new LocationContact(locationPhone.toString(),
locationPin.toString(), TimeUtils.instantFromTimestamp(periodStartTime));
this.validateMessage(locationContact);
return locationContact;
} catch (NoSuchAlgorithmException | InvalidKeySpecException | IllegalStateException | InvalidCipherTextException
| IOException e) {
throw new CleaEncryptionException(e);
......
package fr.inria.clea.lsp;
import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import fr.inria.clea.lsp.exception.CleaInvalidLocationContactMessageException;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class LocationContactValidator {
private Validator validator;
public LocationContactValidator() {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
validator = factory.getValidator();
}
protected void validateMessage(LocationContact message) throws CleaInvalidLocationContactMessageException {
Set<ConstraintViolation<LocationContact>> violations = validator.validate(message);
for (ConstraintViolation<LocationContact> violation : violations) {
log.error(violation.getMessage());
}
if (!violations.isEmpty()) {
throw new CleaInvalidLocationContactMessageException(violations);
}
}
}
......@@ -3,10 +3,14 @@
*/
package fr.inria.clea.lsp;
import java.time.Instant;
import java.util.Objects;
import java.util.UUID;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import fr.inria.clea.lsp.utils.TimeUtils;
import lombok.AllArgsConstructor;
......@@ -16,6 +20,7 @@ import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.SuperBuilder;
import lombok.extern.slf4j.Slf4j;
/**
* LocationSpecificPart (LSP) contents data respecting the CLEA protocol
......@@ -27,10 +32,27 @@ import lombok.experimental.SuperBuilder;
@EqualsAndHashCode
@AllArgsConstructor
@ToString
@Slf4j
public class LocationSpecificPart {
public static final short LOCATION_TEMPORARY_SECRET_KEY_SIZE = 32; // 256 bits
public static final String VERSION_VALIDATION_MESSAGE = "Version should have a value between 0 and 8 (included)";
public static final String TYPE_VALIDATION_MESSAGE = "Type should have a value between 0 and 8 (included)";
public static final String COUNTRY_CODE_VALIDATION_MESSAGE = "Country code should have a value between 0 and 4096 (included)";
public static final String LOCATION_TEMPORARY_PUBLIC_ID_VALIDATION_MESSAGE = "Location temporary public Id must not be null";
public static final String QRCODE_RENEWAL_INTERVAL_VALIDATION_MESSAGE = "QR-code renewal interval exponent compact should have a value between 0 and 32 (included)";
public static final String VENUE_TYPE_VALIDATION_MESSAGE = "Venue type should have a value between 0 and 32 (included)";
public static final String VENUE_CAT1_VALIDATION_MESSAGE = "Venue type should have a value between 0 and 16 (included)";
public static final String VENUE_CAT2_VALIDATION_MESSAGE = "Venue type should have a value between 0 and 16 (included)";
public static final String PERIOD_DURATION_VALIDATION_MESSAGE = "Period duration should have a value between 0 and 255 (included)";
public static final String COMPRESSED_PERIOD_START_TIME_VALIDATION_MESSAGE = "Compressed period start time should have a value between 0 and 16777216 (included)";
public static final String QR_CODE_VALIDITY_START_TIME_VALIDATION_MESSAGE = "QR-code validity start time must not be null";
public static final String LOCATION_TEMPORARY_SECRET_KEY_VALIDATION_MESSAGE = "Location temporary secret key must not be null";
public static final String LOCATION_TEMPORARY_SECRET_KEY_SIZE_VALIDATION_MESSAGE = "Location temporary secret key must have a size of " + LOCATION_TEMPORARY_SECRET_KEY_SIZE + " bytes";
/* Clea protocol version number */
@Builder.Default
@Max(value = 8)
@Min(value = 0, message = VERSION_VALIDATION_MESSAGE)
@Max(value = 8, message = VERSION_VALIDATION_MESSAGE)
protected int version = 0;
/*
......@@ -38,7 +60,8 @@ public class LocationSpecificPart {
* future.
*/
@Builder.Default
@Max(value = 8)
@Min(value = 0, message = TYPE_VALIDATION_MESSAGE)
@Max(value = 8, message = TYPE_VALIDATION_MESSAGE)
protected int type = 0;