From 7bbf4be984fb77f551f2e4b3744f7c073d23ddbb Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Sat, 28 Sep 2024 16:58:42 +0200 Subject: [PATCH] implemented EncryptedUserService Signed-off-by: Stephan Richter --- .../de/srsoftware/oidc/api/UserService.java | 18 +-- .../de/srsoftware/oidc/api/data/User.java | 4 + .../de/srsoftware/oidc/app/Application.java | 11 +- .../build.gradle | 2 +- .../encrypted/EncryptedUserService.java | 69 ++++++++--- .../test/java/EncryptedUserServiceTest.java | 113 ++++++++++++++++++ 6 files changed, 187 insertions(+), 30 deletions(-) create mode 100644 de.srsoftware.oidc.datastore.encrypted/src/test/java/EncryptedUserServiceTest.java diff --git a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/UserService.java b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/UserService.java index a4daccb..e9d7822 100644 --- a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/UserService.java +++ b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/UserService.java @@ -22,13 +22,13 @@ public interface UserService { * @param accessToken * @return */ - public Optional forToken(String accessToken); - public UserService init(User defaultUser); - public List list(); - public Set find(String idOrEmail); - public Optional load(String id); - public Optional load(String username, String password); - public boolean passwordMatches(String plaintextPassword, User user); - public T save(User user); - public T updatePassword(User user, String plaintextPassword); + public Optional forToken(String accessToken); + public UserService init(User defaultUser); + public List list(); + public Set find(String idOrEmail); + public Optional load(String id); + public Optional load(String username, String password); + public boolean passwordMatches(String plaintextPassword, User user); + public UserService save(User user); + public UserService updatePassword(User user, String plaintextPassword); } diff --git a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/data/User.java b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/data/User.java index 4f2fe44..2a35347 100644 --- a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/data/User.java +++ b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/data/User.java @@ -96,6 +96,10 @@ public final class User { return Optional.of(user); } + public Set permissions() { + return Set.copyOf(permissions); + } + public String realName() { return realName; } 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 6ca937f..a44ec03 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 @@ -19,6 +19,7 @@ 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.encrypted.EncryptedUserService; import de.srsoftware.oidc.datastore.file.FileStoreProvider; import de.srsoftware.oidc.datastore.file.PlaintextKeyStore; import de.srsoftware.oidc.datastore.sqlite.*; @@ -128,10 +129,18 @@ public class Application { private static UserService setupUserService(Configuration config, Path defaultFile, FileStoreProvider fileStoreProvider, UuidHasher passHasher) throws SQLException { var userStorageLocation = new File(config.getOrDefault("user_storage",defaultFile)); - return switch (extension(userStorageLocation).toLowerCase()){ + var userService = switch (extension(userStorageLocation).toLowerCase()){ case "db", "sqlite", "sqlite3" -> new SqliteUserService(connectionProvider.get(userStorageLocation),passHasher); default -> fileStoreProvider.get(userStorageLocation); }; + + Optional encryptionKey = config.get(ENCRYPTION_KEY); + + if (encryptionKey.isPresent()){ + var salt = config.getOrDefault(SALT,uuid()); + userService = new EncryptedUserService(userService,encryptionKey.get(),salt,passHasher); + } + return userService; } private static KeyStorage setupKeyStore(Configuration config, Path defaultConfigDir) throws SQLException { diff --git a/de.srsoftware.oidc.datastore.encrypted/build.gradle b/de.srsoftware.oidc.datastore.encrypted/build.gradle index d735cd7..f27b074 100644 --- a/de.srsoftware.oidc.datastore.encrypted/build.gradle +++ b/de.srsoftware.oidc.datastore.encrypted/build.gradle @@ -15,7 +15,7 @@ dependencies { implementation project(':de.srsoftware.oidc.api') implementation 'com.sun.mail:jakarta.mail:2.0.1' implementation project(':de.srsoftware.utils') - + testImplementation project(path: ':de.srsoftware.oidc.api', configuration: "testBundle") } test { diff --git a/de.srsoftware.oidc.datastore.encrypted/src/main/java/de/srsoftware/oidc/datastore/encrypted/EncryptedUserService.java b/de.srsoftware.oidc.datastore.encrypted/src/main/java/de/srsoftware/oidc/datastore/encrypted/EncryptedUserService.java index 893de54..bdcd9b8 100644 --- a/de.srsoftware.oidc.datastore.encrypted/src/main/java/de/srsoftware/oidc/datastore/encrypted/EncryptedUserService.java +++ b/de.srsoftware.oidc.datastore.encrypted/src/main/java/de/srsoftware/oidc/datastore/encrypted/EncryptedUserService.java @@ -1,21 +1,25 @@ +/* © SRSoftware 2024 */ package de.srsoftware.oidc.datastore.encrypted; +import static java.util.Optional.empty; + import de.srsoftware.oidc.api.UserService; import de.srsoftware.oidc.api.data.AccessToken; import de.srsoftware.oidc.api.data.User; -import de.srsoftware.utils.Optionals; - +import de.srsoftware.utils.PasswordHasher; +import java.util.HashMap; import java.util.List; import java.util.Optional; import java.util.Set; -import java.util.stream.Collectors; public class EncryptedUserService extends EncryptedConfig implements UserService { - private final UserService backend; + private final UserService backend; + private final PasswordHasher hasher; - EncryptedUserService(UserService backend, String key, String salt){ - super(key,salt); + public EncryptedUserService(UserService backend, String key, String salt, PasswordHasher passHasher) { + super(key, salt); this.backend = backend; + hasher = passHasher; } @Override @@ -28,18 +32,28 @@ public class EncryptedUserService extends EncryptedConfig implements UserService return backend.consumeToken(accessToken).map(this::decrypt); } - public User decrypt(User secret){ -return secret; + public User decrypt(User secret) { + var decrypted = new User(decrypt(secret.username()), decrypt(secret.hashedPassword()), decrypt(secret.realName()), decrypt(secret.email()), decrypt(secret.uuid())).sessionDuration(secret.sessionDuration()); + secret.permissions().forEach(decrypted::add); + return decrypted; } @Override public UserService delete(User user) { - backend.delete(encrypt(user)); + for (var encryptedUser : backend.list()) { + var decryptedUser = decrypt(encryptedUser); + if (decryptedUser.uuid().equals(user.uuid())) { + backend.delete(encryptedUser); + break; + } + } return this; } - public User encrypt(User plain){ - return plain; + public User encrypt(User plain) { + var encrypted = new User(encrypt(plain.username()), encrypt(plain.hashedPassword()), encrypt(plain.realName()), encrypt(plain.email()), encrypt(plain.uuid())).sessionDuration(plain.sessionDuration()); + plain.permissions().forEach(encrypted::add); + return encrypted; } @Override @@ -60,33 +74,50 @@ return secret; @Override public Set find(String idOrEmail) { - return backend.find(idOrEmail).stream().map(this::decrypt).collect(Collectors.toSet()); + if (idOrEmail == null || idOrEmail.isBlank()) return Set.of(); + var matching = new HashMap(); + for (var encryptedUser : backend.list()) { + var decryptedUser = decrypt(encryptedUser); + if (idOrEmail.equals(decryptedUser.uuid()) || idOrEmail.equals(decryptedUser.email()) || idOrEmail.equals(decryptedUser.username()) || decryptedUser.realName().contains(idOrEmail)) matching.put(decryptedUser.uuid(), decryptedUser); + } + return Set.copyOf(matching.values()); } @Override public Optional load(String id) { - return backend.load(id).map(this::decrypt); + if (id == null || id.isBlank()) return empty(); + for (var encryptedUser : backend.list()) { + var decryptedUser = decrypt(encryptedUser); + if (id.equals(decryptedUser.uuid())) return Optional.of(decryptedUser); + } + return empty(); } @Override public Optional load(String username, String password) { - return backend.load(encrypt(username),encrypt(password)); + if (username == null || username.isBlank()) return empty(); + for (var encryptedUser : backend.list()) { + var decryptedUser = decrypt(encryptedUser); + if (username.equals(decryptedUser.username()) && hasher.matches(password, decryptedUser.hashedPassword())) return Optional.of(decryptedUser); + } + return empty(); } @Override public boolean passwordMatches(String plaintextPassword, User user) { - return backend.passwordMatches(encrypt(plaintextPassword),encrypt(user)); + return hasher.matches(plaintextPassword, user.hashedPassword()); } @Override - public EncryptedUserService save(User user) { + public UserService save(User user) { + delete(user); backend.save(encrypt(user)); return this; } @Override - public EncryptedUserService updatePassword(User user, String plaintextPassword) { - backend.updatePassword(encrypt(user),encrypt(plaintextPassword)); - return this; + public UserService updatePassword(User user, String plaintextPassword) { + var pass = hasher.hash(plaintextPassword, user.uuid()); + return save(user.hashedPassword(pass)); } } diff --git a/de.srsoftware.oidc.datastore.encrypted/src/test/java/EncryptedUserServiceTest.java b/de.srsoftware.oidc.datastore.encrypted/src/test/java/EncryptedUserServiceTest.java new file mode 100644 index 0000000..01fdd02 --- /dev/null +++ b/de.srsoftware.oidc.datastore.encrypted/src/test/java/EncryptedUserServiceTest.java @@ -0,0 +1,113 @@ +/* © SRSoftware 2024 */ +import static de.srsoftware.utils.Optionals.nullable; +import static de.srsoftware.utils.Strings.uuid; + +import de.srsoftware.oidc.api.UserService; +import de.srsoftware.oidc.api.UserServiceTest; +import de.srsoftware.oidc.api.data.AccessToken; +import de.srsoftware.oidc.api.data.User; +import de.srsoftware.oidc.datastore.encrypted.EncryptedUserService; +import de.srsoftware.utils.PasswordHasher; +import java.io.File; +import java.util.*; +import java.util.stream.Collectors; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; + +public class EncryptedUserServiceTest extends UserServiceTest { + private class InMemoryUserService implements UserService { + private final PasswordHasher hasher; + private HashMap users = new HashMap<>(); + + public InMemoryUserService(PasswordHasher hasher) { + this.hasher = hasher; + } + + @Override + public AccessToken accessToken(User user) { + return null; + } + + @Override + public Optional consumeToken(String accessToken) { + return Optional.empty(); + } + + @Override + public UserService delete(User user) { + users.remove(user.uuid()); + return this; + } + + @Override + public Optional forToken(String accessToken) { + return Optional.empty(); + } + + @Override + public UserService init(User defaultUser) { + if (users.isEmpty()) users.put(defaultUser.uuid(), defaultUser); + return this; + } + + @Override + public List list() { + return List.copyOf(users.values()); + } + + @Override + public Set find(String idOrEmail) { + return list().stream().filter(user -> user.uuid().equals(idOrEmail) || user.email().equals(idOrEmail)).collect(Collectors.toSet()); + } + + @Override + public Optional load(String id) { + return nullable(users.get(id)); + } + + @Override + public Optional load(String username, String password) { + return users.values().stream().filter(user -> user.username().equals(username) && passwordMatches(password, user)).findAny(); + } + + @Override + public boolean passwordMatches(String plaintextPassword, User user) { + return hasher.matches(plaintextPassword, user.hashedPassword()); + } + + @Override + public UserService save(User user) { + users.put(user.uuid(), user); + return this; + } + + @Override + public UserService updatePassword(User user, String plaintextPassword) { + var old = users.get(user.uuid()); + save(user.hashedPassword(hasher.hash(plaintextPassword, uuid()))); + return this; + } + } + private File storage = new File("/tmp/" + UUID.randomUUID()); + private UserService userService; + private String key, salt; + + @AfterEach + public void tearDown() { + if (storage.exists()) storage.delete(); + } + + @BeforeEach + public void setup() { + tearDown(); + key = uuid(); + salt = uuid(); + InMemoryUserService backend = new InMemoryUserService(hasher()); + userService = new EncryptedUserService(backend, key, salt, hasher()); + } + + @Override + protected UserService userService() { + return userService; + } +}