Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,114 @@
import com.hpms.domain.Patient;
import com.hpms.repositories.PatientRepository;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Base64;
import java.util.concurrent.ConcurrentHashMap;
import java.util.UUID;

import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;

public class DatabasePatientRepository implements PatientRepository {
private static final String TRANSFORMATION = "AES/GCM/NoPadding";
private static final int AES_KEY_SIZE_BYTES = 32;
private static final int GCM_IV_SIZE_BYTES = 12;
private static final int GCM_TAG_LENGTH_BITS = 128;
private static final String KEY_SEED = "HospitalPatientMonitoringSystem:patient-data-encryption:v1";
private static final SecureRandom SECURE_RANDOM = new SecureRandom();

private final Map<UUID, String> encryptedPatients = new ConcurrentHashMap<>();

@Override
public void save(Patient entity) {
throw new UnsupportedOperationException("DatabasePatientRepository is a future stub and not implemented yet.");
encryptedPatients.put(entity.getPatientId(), encrypt(entity));
}

@Override
public Optional<Patient> findById(UUID id) {
throw new UnsupportedOperationException("DatabasePatientRepository is a future stub and not implemented yet.");
return Optional.ofNullable(encryptedPatients.get(id))
.map(this::decrypt);
}

@Override
public List<Patient> findAll() {
throw new UnsupportedOperationException("DatabasePatientRepository is a future stub and not implemented yet.");
return encryptedPatients.values().stream()
.map(this::decrypt)
.toList();
}

@Override
public void delete(UUID id) {
throw new UnsupportedOperationException("DatabasePatientRepository is a future stub and not implemented yet.");
encryptedPatients.remove(id);
}

private String encrypt(Patient patient) {
try {
byte[] plainBytes = serialize(patient);
byte[] iv = new byte[GCM_IV_SIZE_BYTES];
SECURE_RANDOM.nextBytes(iv);

Cipher cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.ENCRYPT_MODE, getKey(), new GCMParameterSpec(GCM_TAG_LENGTH_BITS, iv));
byte[] cipherBytes = cipher.doFinal(plainBytes);

byte[] combined = new byte[iv.length + cipherBytes.length];
System.arraycopy(iv, 0, combined, 0, iv.length);
System.arraycopy(cipherBytes, 0, combined, iv.length, cipherBytes.length);
return Base64.getEncoder().encodeToString(combined);
} catch (GeneralSecurityException | IOException ex) {
throw new IllegalStateException("Unable to encrypt patient record.", ex);
}
}

private Patient decrypt(String encodedPayload) {
try {
byte[] combined = Base64.getDecoder().decode(encodedPayload.getBytes(StandardCharsets.UTF_8));
byte[] iv = new byte[GCM_IV_SIZE_BYTES];
byte[] cipherBytes = new byte[combined.length - GCM_IV_SIZE_BYTES];
System.arraycopy(combined, 0, iv, 0, GCM_IV_SIZE_BYTES);
System.arraycopy(combined, GCM_IV_SIZE_BYTES, cipherBytes, 0, cipherBytes.length);

Cipher cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.DECRYPT_MODE, getKey(), new GCMParameterSpec(GCM_TAG_LENGTH_BITS, iv));
byte[] plainBytes = cipher.doFinal(cipherBytes);
return deserialize(plainBytes);
} catch (GeneralSecurityException | IOException | ClassNotFoundException ex) {
throw new IllegalStateException("Unable to decrypt patient record.", ex);
}
}

private byte[] serialize(Patient patient) throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream)) {
objectOutputStream.writeObject(patient);
}
return outputStream.toByteArray();
}

private Patient deserialize(byte[] plainBytes) throws IOException, ClassNotFoundException {
try (ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(plainBytes))) {
return (Patient) objectInputStream.readObject();
}
}

private SecretKeySpec getKey() throws GeneralSecurityException {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] keyBytes = digest.digest(KEY_SEED.getBytes(StandardCharsets.UTF_8));
byte[] aesKey = new byte[AES_KEY_SIZE_BYTES];
System.arraycopy(keyBytes, 0, aesKey, 0, aesKey.length);
return new SecretKeySpec(aesKey, "AES");
}
}
5 changes: 4 additions & 1 deletion src/com/hpms/domain/Alert.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package com.hpms.domain;

import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.UUID;

public class Alert {
public class Alert implements Serializable {
private static final long serialVersionUID = 1L;

private final UUID alertId;
private final UUID patientId;
private final UUID vitalReadingId;
Expand Down
5 changes: 4 additions & 1 deletion src/com/hpms/domain/AlertThreshold.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package com.hpms.domain;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

public class AlertThreshold {
public class AlertThreshold implements Serializable {
private static final long serialVersionUID = 1L;

private final UUID thresholdId;
private UUID patientId;
private final String vitalType;
Expand Down
5 changes: 4 additions & 1 deletion src/com/hpms/domain/Patient.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package com.hpms.domain;

import java.io.Serializable;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;

public class Patient {
public class Patient implements Serializable {
private static final long serialVersionUID = 1L;

private final UUID patientId;
private final String firstName;
private final String lastName;
Expand Down
5 changes: 4 additions & 1 deletion src/com/hpms/domain/Report.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package com.hpms.domain;

import java.io.Serializable;
import java.nio.file.Path;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.UUID;

public class Report {
public class Report implements Serializable {
private static final long serialVersionUID = 1L;

private final UUID reportId;
private final UUID patientId;
private final UUID generatedById;
Expand Down
5 changes: 4 additions & 1 deletion src/com/hpms/domain/VitalReading.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package com.hpms.domain;

import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.Comparator;
import java.util.List;
import java.util.UUID;

public class VitalReading {
public class VitalReading implements Serializable {
private static final long serialVersionUID = 1L;

private final UUID readingId;
private final UUID patientId;
private final double heartRate;
Expand Down
2 changes: 2 additions & 0 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
server.ssl.protocol=TLS
server.ssl.enabled-protocols=TLSv1.2,TLSv1.3
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.hpms.repositories.database;

import com.hpms.domain.Patient;
import org.junit.jupiter.api.Test;

import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.time.LocalDate;
import java.util.Base64;
import java.util.Map;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

class DatabasePatientRepositoryTest {
@Test
void savesAndRestoresEncryptedPatientRecords() throws Exception {
DatabasePatientRepository repository = new DatabasePatientRepository();
Patient patient = new Patient("Lena", "Moyo", LocalDate.of(1999, 3, 10), "Flu");
patient.register();
repository.save(patient);

Optional<Patient> restored = repository.findById(patient.getPatientId());

assertTrue(restored.isPresent());
assertEquals(patient.getPatientId(), restored.orElseThrow().getPatientId());
assertEquals("Flu", restored.orElseThrow().getDiagnosis());
assertEquals("ADMITTED", restored.orElseThrow().getStatus());

Field storageField = DatabasePatientRepository.class.getDeclaredField("encryptedPatients");
storageField.setAccessible(true);
@SuppressWarnings("unchecked")
Map<?, String> storage = (Map<?, String>) storageField.get(repository);
String storedCiphertext = storage.values().iterator().next();

String plainSerialized = Base64.getEncoder().encodeToString(serialize(patient));

assertNotEquals(plainSerialized, storedCiphertext);
assertFalse(storedCiphertext.contains("Lena"));
assertFalse(storedCiphertext.contains("Moyo"));
assertFalse(storedCiphertext.contains("Flu"));
}

@Test
void deleteRemovesEncryptedRecord() {
DatabasePatientRepository repository = new DatabasePatientRepository();
Patient patient = new Patient("Ava", "Smith", LocalDate.of(1990, 1, 1), "Asthma");

repository.save(patient);
repository.delete(patient.getPatientId());

assertTrue(repository.findById(patient.getPatientId()).isEmpty());
assertTrue(repository.findAll().isEmpty());
}

private byte[] serialize(Patient patient) throws Exception {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream)) {
objectOutputStream.writeObject(patient);
}
return outputStream.toByteArray();
}
}
Loading