Attention une mise à jour du service Gitlab va être effectuée le mardi 30 novembre entre 17h30 et 18h00. Cette mise à jour va générer une interruption du service dont nous ne maîtrisons pas complètement la durée mais qui ne devrait pas excéder quelques minutes. Cette mise à jour intermédiaire en version 14.0.12 nous permettra de rapidement pouvoir mettre à votre disposition une version plus récente.

Commit b2be6768 authored by calocedre TAC's avatar calocedre TAC
Browse files

add validation of messages before encoding

parent 85a367f0
......@@ -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>
......
......@@ -2,26 +2,35 @@ package fr.inria.clea.lsp;
import java.time.Instant;
import javax.validation.constraints.Max;
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)
@Size(min = 6, max = 6,
message = PIN_VALIDATION_MESSAGE)
String locationPin;
/* Starting time of the period in seconds */
@NotNull(message= PERIOD_START_TIME_VALIDATION_MESSAGE)
@Setter(AccessLevel.PROTECTED)
Instant periodStartTime;
}
......@@ -5,12 +5,19 @@ 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;
......@@ -18,14 +25,18 @@ import lombok.extern.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 CleaCryptoException {
this.validateMessage(message);
try {
byte[] messageBinary = this.getBinaryMessage(message);
byte[] encryptedLocationContactMessage = cleaEncoder.encrypt(null, messageBinary, manualContactTracingAuthorityPublicKey);
......@@ -39,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:
......@@ -111,8 +132,10 @@ public class LocationContactMessageEncoder {
/* t_periodStart (32 bits) */
long periodStartTime = bitLocationContactMessage.getNextLong(32);
return new LocationContact(locationPhone.toString(),
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);
}
}
}
......@@ -8,6 +8,9 @@ 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;
......@@ -17,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
......@@ -28,10 +32,27 @@ import lombok.experimental.SuperBuilder;
@EqualsAndHashCode
@AllArgsConstructor
@ToString
@Slf4j
public class LocationSpecificPart {
public static final short LOCATION_TEMPORARY_SECRET_KEY_SIZE = 64;
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;
/*
......@@ -39,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;
/*
......@@ -47,8 +69,9 @@ public class LocationSpecificPart {
* France
*/
@Builder.Default
@Max(value = 4096)
protected int countryCode = 33;
@Min(value = 0, message = COUNTRY_CODE_VALIDATION_MESSAGE)
@Max(value = 4096, message = COUNTRY_CODE_VALIDATION_MESSAGE)
protected int countryCode = 250;
/* regular users or staff member of the location */
protected boolean staff;
......@@ -58,41 +81,52 @@ public class LocationSpecificPart {
* given location at a given period.
*/
@Setter
@NotNull(message= LOCATION_TEMPORARY_PUBLIC_ID_VALIDATION_MESSAGE)
protected UUID locationTemporaryPublicId;
/*
* qrCodeRenewalInterval value in a compact manner, as the exponent of a power
* of two.
*/
@Max(value = 32)
@Min(value = 0, message = QRCODE_RENEWAL_INTERVAL_VALIDATION_MESSAGE)
@Max(value = 32, message = QRCODE_RENEWAL_INTERVAL_VALIDATION_MESSAGE)
protected int qrCodeRenewalIntervalExponentCompact;
/* Type of the location/venue */
@Max(value = 32)
@Min(value = 0, message = VENUE_TYPE_VALIDATION_MESSAGE)
@Max(value = 32, message = VENUE_TYPE_VALIDATION_MESSAGE)
protected int venueType;
/* Reserved: a first level of venue category */
@Max(value = 16)
@Min(value = 0, message = VENUE_CAT1_VALIDATION_MESSAGE)
@Max(value = 16, message = VENUE_CAT1_VALIDATION_MESSAGE)
protected int venueCategory1;
/* Reserved: a second level of venue category */
@Max(value = 16)
@Min(value = 0, message = VENUE_CAT2_VALIDATION_MESSAGE)
@Max(value = 16, message = VENUE_CAT2_VALIDATION_MESSAGE)
protected int venueCategory2;
/* Duration, in terms of number of hours, of the period */
@Max(value = 255)
@Min(value = 0, message = PERIOD_DURATION_VALIDATION_MESSAGE)
@Max(value = 255, message = PERIOD_DURATION_VALIDATION_MESSAGE)
protected int periodDuration;
/* Starting time of the period in a compressed manner (round hour) */
@Max(value = 16777216)
@Min(value = 0, message = COMPRESSED_PERIOD_START_TIME_VALIDATION_MESSAGE)
@Max(value = 16777216, message = COMPRESSED_PERIOD_START_TIME_VALIDATION_MESSAGE)
protected int compressedPeriodStartTime;
/* Starting time of the QR code validity timespan in seconds */
@Setter
@NotNull(message= QR_CODE_VALIDITY_START_TIME_VALIDATION_MESSAGE)
protected Instant qrCodeValidityStartTime;
/* Temporary location key for the period */
@Setter
@NotNull(message= LOCATION_TEMPORARY_SECRET_KEY_VALIDATION_MESSAGE)
@Size(min = LOCATION_TEMPORARY_SECRET_KEY_SIZE,
max = LOCATION_TEMPORARY_SECRET_KEY_SIZE, message= LOCATION_TEMPORARY_SECRET_KEY_SIZE_VALIDATION_MESSAGE)
protected byte[] locationTemporarySecretKey;
@Setter
......@@ -110,6 +144,10 @@ public class LocationSpecificPart {
}
public void setPeriodStartTime(Instant periodStartTime) {
if (Objects.isNull(periodStartTime)) {
log.error("Period start time not set. Null value provided");
return;
}
long periodStartTimeAsNtpTimestamp = TimeUtils.ntpTimestampFromInstant(periodStartTime);
this.compressedPeriodStartTime = (int) (periodStartTimeAsNtpTimestamp / TimeUtils.NB_SECONDS_PER_HOUR);
}
......
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.CleaInvalidLocationMessageException;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class LocationSpecificPartContactValidator {
private Validator validator;
public LocationSpecificPartContactValidator() {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
validator = factory.getValidator();
}
protected void validateMessage(LocationSpecificPart message) throws CleaInvalidLocationMessageException {
Set<ConstraintViolation<LocationSpecificPart>> violations = validator.validate(message);
for (ConstraintViolation<LocationSpecificPart> violation : violations) {
log.error(violation.getMessage());
}
if (!violations.isEmpty()) {
throw new CleaInvalidLocationMessageException(violations);
}
}
}
......@@ -5,10 +5,18 @@ package fr.inria.clea.lsp;
import java.io.IOException;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
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.CleaInvalidLocationMessageException;
import fr.inria.clea.lsp.utils.TimeUtils;
import lombok.extern.slf4j.Slf4j;
......@@ -22,9 +30,12 @@ public class LocationSpecificPartEncoder {
/* ECIES crytography */
private CleaEciesEncoder cleaEciesEncoder;
private String serverAuthorityPublicKey;
private Validator validator;
public LocationSpecificPartEncoder() {
this.cleaEciesEncoder = new CleaEciesEncoder();
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
validator = factory.getValidator();
}
public LocationSpecificPartEncoder(String serverAuthorityPublicKey) {
......@@ -32,10 +43,11 @@ public class LocationSpecificPartEncoder {
this.serverAuthorityPublicKey = serverAuthorityPublicKey;
}
public byte[] encode(LocationSpecificPart locationSpecificPart) throws CleaEncryptionException {
public byte[] encode(LocationSpecificPart locationSpecificPart) throws CleaCryptoException {
if (Objects.isNull(serverAuthorityPublicKey)) {
throw new CleaEncryptionException("Cannot encrypt, serverAuthorityPublicKey is null!");
}
this.validateMessage(locationSpecificPart);
byte[] header = this.binaryEncodedHeader(locationSpecificPart);
byte[] msg = this.binaryEncodedMessage(locationSpecificPart);
byte[] encryptedLocationSpecificPart = this.encrypt(header, msg, this.serverAuthorityPublicKey);
......@@ -145,4 +157,13 @@ public class LocationSpecificPartEncoder {
}
}
protected void validateMessage(LocationSpecificPart message) throws CleaInvalidLocationMessageException {
Set<ConstraintViolation<LocationSpecificPart>> violations = validator.validate(message);
for (ConstraintViolation<LocationSpecificPart> violation : violations) {
log.error(violation.getMessage());
}
if (!violations.isEmpty()) {
throw new CleaInvalidLocationMessageException(violations);
}
}
}
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.CleaInvalidLocationMessageException;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class LocationSpecificPartValidator {
private Validator validator;
public LocationSpecificPartValidator() {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
validator = factory.getValidator();
}
protected void validateMessage(LocationSpecificPart message) throws CleaInvalidLocationMessageException {
Set<ConstraintViolation<LocationSpecificPart>> violations = validator.validate(message);
for (ConstraintViolation<LocationSpecificPart> violation : violations) {
log.error(violation.getMessage());
}
if (!violations.isEmpty()) {
throw new CleaInvalidLocationMessageException(violations);
}
}
}
package fr.inria.clea.lsp.exception;
import java.util.Set;
import javax.validation.ConstraintViolation;
import fr.inria.clea.lsp.LocationContact;
import lombok.Getter;
public class CleaInvalidLocationContactMessageException extends CleaCryptoException {
private static final long serialVersionUID = 1L;
@Getter
Set<ConstraintViolation<LocationContact>> violations;
public CleaInvalidLocationContactMessageException(Set<ConstraintViolation<LocationContact>> violations) {
super();
this.violations = violations;
}
@Override
public String getMessage() {
return violations.toString();
}
}
package fr.inria.clea.lsp.exception;
import java.util.Set;
import javax.validation.ConstraintViolation;
import fr.inria.clea.lsp.LocationSpecificPart;
import lombok.Getter;
public class CleaInvalidLocationMessageException extends CleaCryptoException {
private static final long serialVersionUID = 1L;
@Getter
Set<ConstraintViolation<LocationSpecificPart>> violations;
public CleaInvalidLocationMessageException(Set<ConstraintViolation<LocationSpecificPart>> violations) {
super();
this.violations = violations;
}
}
package fr.inria.clea.lsp;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.time.Instant;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import fr.inria.clea.lsp.LocationContact.LocationContactBuilder;
import fr.inria.clea.lsp.exception.CleaInvalidLocationContactMessageException;
public class LocationContactValidationTest {
private LocationContactBuilder locationContactBuilder;
@BeforeEach
public void setUp() {
Instant periodStartTime = Instant.now();
locationContactBuilder = LocationContact.builder()
.locationPhone("061122334455")
.locationPin("123456")
.periodStartTime(periodStartTime);
}
@Test
public void testWhenLocationContactIsValidThenValidationSucceeds() throws CleaInvalidLocationContactMessageException {
new LocationContactValidator().validateMessage(locationContactBuilder.build());
}
@Test
public void testWhenPinCodeHasMoreThan6DigitsThenValidationFails() {
LocationContact locationContact = locationContactBuilder.locationPin("1234567").build();
CleaInvalidLocationContactMessageException exception = assertThrows(CleaInvalidLocationContactMessageException.class, () -> {
new LocationContactValidator().validateMessage(locationContact);
});
assertThat(exception.getViolations().size()).isEqualTo(1);
assertThat(exception.getViolations()).anyMatch(violation -> violation.getMessage().equals(LocationContact.PIN_VALIDATION_MESSAGE));
}
@Test
public void testWhenPinCodeHasLessThan6DigitsThenValidationFails() {
LocationContact locationContact = locationContactBuilder.locationPin("12345").build();
CleaInvalidLocationContactMessageException exception = assertThrows(CleaInvalidLocationContactMessageException.class, () -> {
new LocationContactValidator().validateMessage(locationContact);
});
assertThat(exception.getViolations().size()).isEqualTo(1);
assertThat(exception.getViolations()).anyMatch(violation -> violation.getMessage().equals(LocationContact.PIN_VALIDATION_MESSAGE));
}
@Test
public void testWhenLocationPhoneIsEmptyThenValidationFails() {
LocationContact locationContact = locationContactBuilder.locationPhone("").build();
CleaInvalidLocationContactMessageException exception = assertThrows(CleaInvalidLocationContactMessageException.class, () -> {
new LocationContactValidator().validateMessage(locationContact);
});
assertThat(exception.getViolations().size()).isEqualTo(1);
assertThat(exception.getViolations()).anyMatch(violation -> violation.getMessage().equals(LocationContact.PHONE_VALIDATION_MESSAGE));
}
@Test
public void testWhenLocationPhoneIsNullThenValidationFails() {
LocationContact locationContact = locationContactBuilder.locationPhone(null).build();
CleaInvalidLocationContactMessageException exception = assertThrows(CleaInvalidLocationContactMessageException.class, () -> {
new LocationContactValidator().validateMessage(locationContact);
});
assertThat(exception.getViolations().size()).isEqualTo(1);
assertThat(exception.getViolations()).anyMatch(violation -> violation.getMessage().equals(LocationContact.PHONE_VALIDATION_MESSAGE));
}
@Test
public void testWhenPeriodStartTimeIsNullThenValidationFails() {
LocationContact locationContact = locationContactBuilder.periodStartTime(null).build();
CleaInvalidLocationContactMessageException exception = assertThrows(CleaInvalidLocationContactMessageException.class, () -> {
new LocationContactValidator().validateMessage(locationContact);
});
assertThat(exception.getViolations().size()).isEqualTo(1);
assertThat(exception.getViolations()).anyMatch(violation -> violation.getMessage().equals(LocationContact.PERIOD_START_TIME_VALIDATION_MESSAGE));
}
}
package fr.inria.clea.lsp;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.time.Instant;
import java.util.Random;
import java.util.UUID;
import org.junit.jupiter.api.BeforeEach;