From b91de1ea9e1372f21bdd41fde04c838b36604add Mon Sep 17 00:00:00 2001 From: skiet88 Date: Mon, 8 Jun 2026 00:53:30 +0200 Subject: [PATCH] Implement AES-256 patient data encryption --- .../database/DatabasePatientRepository.java | 94 ++++++++++++++++++- src/com/hpms/domain/Alert.java | 5 +- src/com/hpms/domain/AlertThreshold.java | 5 +- src/com/hpms/domain/Patient.java | 5 +- src/com/hpms/domain/Report.java | 5 +- src/com/hpms/domain/VitalReading.java | 5 +- src/main/resources/application.properties | 2 + .../DatabasePatientRepositoryTest.java | 67 +++++++++++++ 8 files changed, 179 insertions(+), 9 deletions(-) create mode 100644 src/main/resources/application.properties create mode 100644 tests/com/hpms/repositories/database/DatabasePatientRepositoryTest.java diff --git a/repositories/com/hpms/repositories/database/DatabasePatientRepository.java b/repositories/com/hpms/repositories/database/DatabasePatientRepository.java index 597c43e..a50493b 100644 --- a/repositories/com/hpms/repositories/database/DatabasePatientRepository.java +++ b/repositories/com/hpms/repositories/database/DatabasePatientRepository.java @@ -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 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 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 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"); } } \ No newline at end of file diff --git a/src/com/hpms/domain/Alert.java b/src/com/hpms/domain/Alert.java index 9dae029..4bf6878 100644 --- a/src/com/hpms/domain/Alert.java +++ b/src/com/hpms/domain/Alert.java @@ -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; diff --git a/src/com/hpms/domain/AlertThreshold.java b/src/com/hpms/domain/AlertThreshold.java index 850696d..388f2d3 100644 --- a/src/com/hpms/domain/AlertThreshold.java +++ b/src/com/hpms/domain/AlertThreshold.java @@ -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; diff --git a/src/com/hpms/domain/Patient.java b/src/com/hpms/domain/Patient.java index dcea4d4..5c52877 100644 --- a/src/com/hpms/domain/Patient.java +++ b/src/com/hpms/domain/Patient.java @@ -1,5 +1,6 @@ package com.hpms.domain; +import java.io.Serializable; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.ArrayList; @@ -7,7 +8,9 @@ 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; diff --git a/src/com/hpms/domain/Report.java b/src/com/hpms/domain/Report.java index 6ca8e57..c255b59 100644 --- a/src/com/hpms/domain/Report.java +++ b/src/com/hpms/domain/Report.java @@ -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; diff --git a/src/com/hpms/domain/VitalReading.java b/src/com/hpms/domain/VitalReading.java index 72b5a5a..c79e30f 100644 --- a/src/com/hpms/domain/VitalReading.java +++ b/src/com/hpms/domain/VitalReading.java @@ -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; diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..1c99e60 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,2 @@ +server.ssl.protocol=TLS +server.ssl.enabled-protocols=TLSv1.2,TLSv1.3 \ No newline at end of file diff --git a/tests/com/hpms/repositories/database/DatabasePatientRepositoryTest.java b/tests/com/hpms/repositories/database/DatabasePatientRepositoryTest.java new file mode 100644 index 0000000..d416d0c --- /dev/null +++ b/tests/com/hpms/repositories/database/DatabasePatientRepositoryTest.java @@ -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 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 storage = (Map) 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(); + } +} \ No newline at end of file