diff --git a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Constants.java b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Constants.java index d38b7d1..5c22717 100644 --- a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Constants.java +++ b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Constants.java @@ -18,6 +18,7 @@ public class Constants { public static final String CONFIG_PATH = "LIGHTOIDC_CONFIG_PATH"; public static final String CONFIRMED = "confirmed"; public static final String DAYS = "days"; + public static final String ENCRYPTION_KEY = "encryption_key"; public static final String ERROR_DESCRIPTION = "error_description"; public static final String EXPIRATION = "expiration"; public static final String EXPIRES_IN = "expires_in"; @@ -39,6 +40,7 @@ public class Constants { public static final String REDIRECT_URIS = "redirect_uris"; public static final String REQUEST_NOT_SUPPORTED = "request_not_supported"; public static final String RESPONSE_TYPE = "response_type"; + public static final String SALT = "salt"; public static final String SCOPE = "scope"; public static final String SECRET = "secret"; public static final String SESSION_DURATION = "session_duration"; diff --git a/de.srsoftware.oidc.app/build.gradle b/de.srsoftware.oidc.app/build.gradle index d7c9ed9..5bf4ebf 100644 --- a/de.srsoftware.oidc.app/build.gradle +++ b/de.srsoftware.oidc.app/build.gradle @@ -19,6 +19,7 @@ dependencies { implementation project(':de.srsoftware.oidc.backend') implementation project(':de.srsoftware.oidc.web') implementation project(':de.srsoftware.utils') + implementation project(':de.srsoftware.oidc.datastore.encrypted') implementation project(':de.srsoftware.oidc.datastore.file') implementation project(':de.srsoftware.oidc.datastore.sqlite') implementation 'org.json:json:20240303' diff --git a/de.srsoftware.oidc.app/src/main/java/de/srsoftware/oidc/app/Application.java b/de.srsoftware.oidc.app/src/main/java/de/srsoftware/oidc/app/Application.java index 3c742ca..28e3660 100644 --- a/de.srsoftware.oidc.app/src/main/java/de/srsoftware/oidc/app/Application.java +++ b/de.srsoftware.oidc.app/src/main/java/de/srsoftware/oidc/app/Application.java @@ -18,6 +18,7 @@ import de.srsoftware.logging.ColorLogger; import de.srsoftware.oidc.api.*; import de.srsoftware.oidc.api.data.User; import de.srsoftware.oidc.backend.*; +import de.srsoftware.oidc.datastore.encrypted.EncryptedMailConfig; import de.srsoftware.oidc.datastore.file.FileStoreProvider; import de.srsoftware.oidc.datastore.file.PlaintextKeyStore; import de.srsoftware.oidc.datastore.sqlite.*; @@ -110,10 +111,19 @@ public class Application { private static MailConfig setupMailConfig(Configuration config, Path defaultFile, FileStoreProvider fileStoreProvider) throws SQLException { var mailConfigLocation = new File(config.getOrDefault("mail_config_storage",defaultFile)); - return switch (extension(mailConfigLocation)){ + var mailConfig = switch (extension(mailConfigLocation)){ case "db", "sqlite", "sqlite3" -> new SqliteMailConfig(connectionProvider.get(mailConfigLocation)); default -> fileStoreProvider.get(mailConfigLocation); }; + + Optional encryptionKey = config.get(ENCRYPTION_KEY); + var salt = config.getOrDefault(SALT,uuid()); + + + if (encryptionKey.isPresent()){ + mailConfig = new EncryptedMailConfig(mailConfig,encryptionKey.get(),salt); + } + return mailConfig; } private static UserService setupUserService(Configuration config, Path defaultFile, FileStoreProvider fileStoreProvider, UuidHasher passHasher) throws SQLException { diff --git a/de.srsoftware.oidc.app/src/main/java/de/srsoftware/oidc/app/Configuration.java b/de.srsoftware.oidc.app/src/main/java/de/srsoftware/oidc/app/Configuration.java index c8dbcc0..dbc5d31 100644 --- a/de.srsoftware.oidc.app/src/main/java/de/srsoftware/oidc/app/Configuration.java +++ b/de.srsoftware.oidc.app/src/main/java/de/srsoftware/oidc/app/Configuration.java @@ -1,11 +1,14 @@ /* © SRSoftware 2024 */ package de.srsoftware.oidc.app; +import static java.util.Optional.empty; + import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Optional; import org.json.JSONObject; public class Configuration { @@ -30,6 +33,17 @@ public class Configuration { return json.getString(key); } + public Optional get(String key) { + if (!json.has(key)) return empty(); + var o = json.get(key); + try { + @SuppressWarnings("unchecked") var result = (T)o; + return Optional.of(result); + } catch (Exception e) { + return empty(); + } + } + public Configuration save() { try { Files.writeString(storageFile, json.toString(2)); diff --git a/de.srsoftware.oidc.datastore.encrypted/build.gradle b/de.srsoftware.oidc.datastore.encrypted/build.gradle new file mode 100644 index 0000000..dfeed0c --- /dev/null +++ b/de.srsoftware.oidc.datastore.encrypted/build.gradle @@ -0,0 +1,22 @@ +plugins { + id 'java' +} + +group = 'de.srsoftware' +version = '1.0-SNAPSHOT' + +repositories { + mavenCentral() +} + +dependencies { + testImplementation platform('org.junit:junit-bom:5.10.0') + testImplementation 'org.junit.jupiter:junit-jupiter' + implementation project(':de.srsoftware.oidc.api') + implementation 'com.sun.mail:jakarta.mail:2.0.1' + +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/de.srsoftware.oidc.datastore.encrypted/src/main/java/de/srsoftware/oidc/datastore/encrypted/EncryptedConfig.java b/de.srsoftware.oidc.datastore.encrypted/src/main/java/de/srsoftware/oidc/datastore/encrypted/EncryptedConfig.java new file mode 100644 index 0000000..4720f7f --- /dev/null +++ b/de.srsoftware.oidc.datastore.encrypted/src/main/java/de/srsoftware/oidc/datastore/encrypted/EncryptedConfig.java @@ -0,0 +1,71 @@ +/* © SRSoftware 2024 */ +package de.srsoftware.oidc.datastore.encrypted; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.util.Base64; +import javax.crypto.*; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; + +public class EncryptedConfig { + private final Cipher cipher; + private static final int KEY_LENGTH = 256; + private static final int ITERATION_COUNT = 65536; + private final SecretKeySpec secretKeySpec; + + public EncryptedConfig(String key, String salt) { + try { + SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); + KeySpec spec = new PBEKeySpec(key.toCharArray(), salt.getBytes(), ITERATION_COUNT, KEY_LENGTH); + SecretKey tmp = factory.generateSecret(spec); + secretKeySpec = new SecretKeySpec(tmp.getEncoded(), "AES"); + + cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + } catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeySpecException ex) { + throw new RuntimeException(ex); + } + } + + public String encrypt(String plain) { + SecureRandom secureRandom = new SecureRandom(); + byte[] iv = new byte[16]; + secureRandom.nextBytes(iv); + IvParameterSpec initVector = new IvParameterSpec(iv); + try { + cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, initVector); + byte[] cipherText = cipher.doFinal(plain.getBytes(UTF_8)); + byte[] encryptedData = new byte[iv.length + cipherText.length]; + System.arraycopy(iv, 0, encryptedData, 0, iv.length); + System.arraycopy(cipherText, 0, encryptedData, iv.length, cipherText.length); + + return Base64.getEncoder().encodeToString(encryptedData); + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public String decrypt(String secret) { + byte[] encryptedData = Base64.getDecoder().decode(secret); + byte[] iv = new byte[16]; + System.arraycopy(encryptedData, 0, iv, 0, iv.length); + IvParameterSpec ivspec = new IvParameterSpec(iv); + try { + cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivspec); + byte[] cipherText = new byte[encryptedData.length - 16]; + System.arraycopy(encryptedData, 16, cipherText, 0, cipherText.length); + + byte[] decryptedText = cipher.doFinal(cipherText); + return new String(decryptedText, UTF_8); + + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/de.srsoftware.oidc.datastore.encrypted/src/main/java/de/srsoftware/oidc/datastore/encrypted/EncryptedMailConfig.java b/de.srsoftware.oidc.datastore.encrypted/src/main/java/de/srsoftware/oidc/datastore/encrypted/EncryptedMailConfig.java new file mode 100644 index 0000000..9aea706 --- /dev/null +++ b/de.srsoftware.oidc.datastore.encrypted/src/main/java/de/srsoftware/oidc/datastore/encrypted/EncryptedMailConfig.java @@ -0,0 +1,105 @@ +/* © SRSoftware 2024 */ +package de.srsoftware.oidc.datastore.encrypted; + +/* © SRSoftware 2024 */ + +import static de.srsoftware.oidc.api.Constants.*; + +import de.srsoftware.oidc.api.MailConfig; +import jakarta.mail.Authenticator; +import jakarta.mail.PasswordAuthentication; + + +public class EncryptedMailConfig extends EncryptedConfig implements MailConfig { + private final MailConfig storage; + private Authenticator auth; + + public EncryptedMailConfig(MailConfig storage, String encryotionKey, String salt) { + super(encryotionKey, salt); + this.storage = storage; + } + + @Override + public MailConfig save() { + return storage.save(); + } + + @Override + public String senderAddress() { + return decrypt(storage.senderAddress()); + } + + @Override + public MailConfig senderAddress(String newValue) { + storage.senderAddress(encrypt(newValue)); + return this; + } + + @Override + public String senderPassword() { + return decrypt(storage.senderPassword()); + } + + @Override + public MailConfig senderPassword(String newValue) { + storage.senderPassword(encrypt(newValue)); + return this; + } + + @Override + public boolean smtpAuth() { + return storage.smtpAuth(); + } + + @Override + public MailConfig smtpAuth(boolean newValue) { + storage.smtpAuth(newValue); + return this; + } + + @Override + public Authenticator authenticator() { + if (auth == null) { + auth = new Authenticator() { + // override the getPasswordAuthentication method + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(senderAddress(), senderPassword()); + } + }; + } + return auth; + } + + @Override + public String smtpHost() { + return decrypt(storage.smtpHost()); + } + + @Override + public MailConfig smtpHost(String newValue) { + storage.smtpHost(encrypt(newValue)); + return this; + } + + @Override + public int smtpPort() { + return storage.smtpPort(); + } + + @Override + public MailConfig smtpPort(int newValue) { + storage.smtpPort(newValue); + return this; + } + + @Override + public boolean startTls() { + return storage.startTls(); + } + + @Override + public MailConfig startTls(boolean newValue) { + storage.startTls(newValue); + return this; + } +} diff --git a/settings.gradle b/settings.gradle index 0ab7e31..f0ebac6 100644 --- a/settings.gradle +++ b/settings.gradle @@ -8,4 +8,5 @@ include 'de.srsoftware.oidc.datastore.file' include 'de.srsoftware.oidc.web' include 'de.srsoftware.utils' include 'de.srsoftware.oidc.datastore.sqlite' +include 'de.srsoftware.oidc.datastore.encrypted'