diff --git a/.run/PwDemoApplication.run.xml b/.run/PwDemoApplication.run.xml index 0f957cc..b9bcfe9 100644 --- a/.run/PwDemoApplication.run.xml +++ b/.run/PwDemoApplication.run.xml @@ -4,7 +4,6 @@ - \ No newline at end of file diff --git a/.run/Setup tables.run.xml b/.run/Setup tables.run.xml new file mode 100644 index 0000000..1f5966f --- /dev/null +++ b/.run/Setup tables.run.xml @@ -0,0 +1,19 @@ + + + + CREATE TABLE users ( + username VARCHAR(50) NOT NULL PRIMARY KEY, + password VARCHAR(500) NOT NULL, + enabled BOOLEAN NOT NULL +); + +CREATE TABLE authorities ( + username VARCHAR(50) NOT NULL, + authority VARCHAR(50) NOT NULL, + CONSTRAINT fk_authorities_users FOREIGN KEY (username) REFERENCES users(username) +); + +CREATE UNIQUE INDEX ix_auth_username ON authorities (username, authority); + + + \ No newline at end of file diff --git a/presentation_notes.md b/presentation_notes.md index 46cc4df..b50804c 100644 --- a/presentation_notes.md +++ b/presentation_notes.md @@ -40,7 +40,7 @@ Note: Die angeführte Struktur ist die Langfassung von Punkten, die ich in irgen - Character-Set (Buchstaben (klein/groß), Zahlen, (welche) Sonderzeichen?) - Wörter & Namen? → Wenn ja, Tauschregeln von Buchstaben (0 ↔ O, 1 ↔ l ↔ I ↔ !) -### Salt & Pepper +### Salt - "Gratis" Entropy, am Server angehängt - Salt: Unique pro Nutzer @@ -50,6 +50,12 @@ Note: Die angeführte Struktur ist die Langfassung von Punkten, die ich in irgen - Tunable CPU & Memory usage um Brute-Force zu erschweren - Beispiele (Pbkdf, bcrypt, scrypt, ...) + +#### PBKDF2 + +- key = pbkdf2(password, salt, iterations-count, hash-function, derived-key-len) +- + - (Überleitung zu Demo) ### Demo diff --git a/src/main/java/at/nanopenguin/fhtw/itse/pw_demo/CustomPbkdf2PasswordEncoder.java b/src/main/java/at/nanopenguin/fhtw/itse/pw_demo/CustomPbkdf2PasswordEncoder.java new file mode 100644 index 0000000..669aefb --- /dev/null +++ b/src/main/java/at/nanopenguin/fhtw/itse/pw_demo/CustomPbkdf2PasswordEncoder.java @@ -0,0 +1,115 @@ +package at.nanopenguin.fhtw.itse.pw_demo; + +import lombok.SneakyThrows; +import org.springframework.boot.web.servlet.server.Encoding; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.crypto.util.EncodingUtils; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.util.Base64; +import java.util.function.Function; + +public class CustomPbkdf2PasswordEncoder implements PasswordEncoder { + + private static final int SALT_LENGTH = 16; // 128 bits + private static final int ITERATIONS = 1024; + + private final SecureRandom random = new SecureRandom(); + + @Override + public String encode(CharSequence rawPassword) { + + byte[] salt = generateSalt(); + byte[] hash = pbkdf2( + rawPassword.toString().getBytes(StandardCharsets.UTF_8), + salt, + ITERATIONS + ); + + return Base64.getEncoder().encodeToString(salt) + ":" + Base64.getEncoder().encodeToString(hash); + } + + private byte[] generateSalt() { + byte[] salt = new byte[SALT_LENGTH]; + random.nextBytes(salt); + return salt; + } + + private byte[] pbkdf2(byte[] password, byte[] salt, int iterations) { + + byte[] saltWithBlockNum = new byte[salt.length + 4]; + System.arraycopy(salt, 0, saltWithBlockNum, 0, salt.length); + saltWithBlockNum[salt.length + 3] = 1; + + byte[] u = hmac(password, saltWithBlockNum); + byte[] result = u.clone(); + + for (int i = 2; i <= iterations; i++) { + u = hmac(password, u); + xorInPlace(result, u); + } + + return result; + } + + + private void xorInPlace(byte[] a, byte[] b) { + for (int i = 0; i < a.length; i++) { + a[i] ^= b[i]; + } + } + + private byte[] hmac(byte[] key, byte[] message) { + try { + MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); + + // Key preparation + byte[] k = key; + if (key.length > 64) { + k = sha256.digest(key); + sha256.reset(); + } + byte[] keyPadded = new byte[64]; + System.arraycopy(k, 0, keyPadded, 0, k.length); + + // Inner and outer padding + byte[] innerPad = new byte[64]; + byte[] outerPad = new byte[64]; + for (int i = 0; i < 64; i++) { + innerPad[i] = (byte) (keyPadded[i] ^ 0x36); + outerPad[i] = (byte) (keyPadded[i] ^ 0x5C); + } + + // HMAC = SHA256(outerPad || SHA256(innerPad || message)) + sha256.update(innerPad); + sha256.update(message); + byte[] innerHash = sha256.digest(); + + sha256.reset(); + sha256.update(outerPad); + sha256.update(innerHash); + return sha256.digest(); + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public boolean matches(CharSequence rawPassword, String encodedPassword) { + String[] parts = encodedPassword.split(":"); + byte[] salt = Base64.getDecoder().decode(parts[0]); + byte[] storedHash = Base64.getDecoder().decode(parts[1]); + byte[] testHash = pbkdf2(rawPassword.toString().getBytes(StandardCharsets.UTF_8), salt, ITERATIONS); + + int diff = 0; + for (int i = 0; i < 32; i++) { + diff |= storedHash[i] ^ testHash[i]; + } + return diff == 0; + } +} diff --git a/src/main/java/at/nanopenguin/fhtw/itse/pw_demo/RegistrationController.java b/src/main/java/at/nanopenguin/fhtw/itse/pw_demo/RegistrationController.java new file mode 100644 index 0000000..e350bec --- /dev/null +++ b/src/main/java/at/nanopenguin/fhtw/itse/pw_demo/RegistrationController.java @@ -0,0 +1,38 @@ +package at.nanopenguin.fhtw.itse.pw_demo; + +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.provisioning.JdbcUserDetailsManager; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; + +import java.util.Collections; + +@Controller +@RequiredArgsConstructor +public class RegistrationController { + + private final JdbcUserDetailsManager userDetailsService; + private final SelectablePasswordEncoder encoder; + + @GetMapping("/register") + public String showRegistrationForm(Model model) { + model.addAttribute("user", new UserDTO()); + return "register"; + } + + @PostMapping("/register") + public String registerUser(@ModelAttribute UserDTO user, Model model) { + userDetailsService.createUser( + new User(user.getName(), encoder.encodeSelect(user.getPassword(), user.getEncoder()), Collections.singleton(new SimpleGrantedAuthority("USER"))) + ); + model.addAttribute("user", new UserDTO()); + model.addAttribute("message", "User registered successfully!"); + return "register"; + } +} diff --git a/src/main/java/at/nanopenguin/fhtw/itse/pw_demo/SHA256PasswordEncoder.java b/src/main/java/at/nanopenguin/fhtw/itse/pw_demo/SHA256PasswordEncoder.java new file mode 100644 index 0000000..cd120d7 --- /dev/null +++ b/src/main/java/at/nanopenguin/fhtw/itse/pw_demo/SHA256PasswordEncoder.java @@ -0,0 +1,28 @@ +package at.nanopenguin.fhtw.itse.pw_demo; + +import lombok.SneakyThrows; +import org.springframework.security.crypto.password.PasswordEncoder; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.Base64; + +public class SHA256PasswordEncoder implements PasswordEncoder { + + MessageDigest encoder; + + @SneakyThrows // just a demo, so we can throw best practices out of the window :) + public SHA256PasswordEncoder() { + encoder = MessageDigest.getInstance("SHA-256"); + } + + @Override + public String encode(CharSequence rawPassword) { + return Base64.getEncoder().encodeToString(encoder.digest(rawPassword.toString().getBytes(StandardCharsets.UTF_8))); + } + + @Override + public boolean matches(CharSequence rawPassword, String encodedPassword) { + return String.valueOf(this.encode(rawPassword)).equals(encodedPassword); + } +} diff --git a/src/main/java/at/nanopenguin/fhtw/itse/pw_demo/SecurityConfig.java b/src/main/java/at/nanopenguin/fhtw/itse/pw_demo/SecurityConfig.java index 78365b9..dcb3980 100644 --- a/src/main/java/at/nanopenguin/fhtw/itse/pw_demo/SecurityConfig.java +++ b/src/main/java/at/nanopenguin/fhtw/itse/pw_demo/SecurityConfig.java @@ -18,6 +18,7 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.*; import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder; import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.provisioning.JdbcUserDetailsManager; import org.springframework.security.web.SecurityFilterChain; import javax.sql.DataSource; @@ -35,6 +36,7 @@ public class SecurityConfig { http .formLogin(Customizer.withDefaults()) .authorizeHttpRequests(authorize -> authorize + .requestMatchers("/register").permitAll() .anyRequest().authenticated() ); @@ -42,25 +44,24 @@ public class SecurityConfig { } @Bean - public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) { - UserDetailsService userDetailsService = new InMemoryUserDetailsManager( // TODO: JDBC - new User("Beni", passwordEncoder.encode("password"), Set.of(new SimpleGrantedAuthority("USER"))) - ); - - return userDetailsService; + public JdbcUserDetailsManager jdbcUserDetailsManager(DataSource dataSource) { + return new JdbcUserDetailsManager(dataSource); } @Bean - public PasswordEncoder passwordEncoder() { + public SelectablePasswordEncoder passwordEncoder() { String defaultEncoder = "pbkdf2"; Map encoders = new HashMap<>(); encoders.put("noop", NoOpPasswordEncoder.getInstance()); - encoders.put("pbkdf2", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8()); - encoders.put("scrypt", SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8()); - encoders.put("argon2", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8()); - encoders.put("bcrypt", new BCryptPasswordEncoder()); - encoders.put("sha256", new StandardPasswordEncoder()); - return new DelegatingPasswordEncoder(defaultEncoder, encoders); + encoders.put("sha256", new SHA256PasswordEncoder()); + encoders.put("md5", new MessageDigestPasswordEncoder("md5")); + encoders.put("pbkdf2", new CustomPbkdf2PasswordEncoder()); + //encoders.put("pbkdf2", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8()); + //encoders.put("scrypt", SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8()); + //encoders.put("argon2", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8()); + //encoders.put("bcrypt", new BCryptPasswordEncoder()); + //encoders.put("sha256", new StandardPasswordEncoder()); + return new SelectablePasswordEncoder(defaultEncoder, encoders); } } diff --git a/src/main/java/at/nanopenguin/fhtw/itse/pw_demo/SelectablePasswordEncoder.java b/src/main/java/at/nanopenguin/fhtw/itse/pw_demo/SelectablePasswordEncoder.java new file mode 100644 index 0000000..577d9e2 --- /dev/null +++ b/src/main/java/at/nanopenguin/fhtw/itse/pw_demo/SelectablePasswordEncoder.java @@ -0,0 +1,144 @@ +package at.nanopenguin.fhtw.itse.pw_demo; + +import org.springframework.security.crypto.password.PasswordEncoder; + +import java.util.HashMap; +import java.util.Map; + +public class SelectablePasswordEncoder implements PasswordEncoder { + private static final String DEFAULT_ID_PREFIX = "{"; + private static final String DEFAULT_ID_SUFFIX = "}"; + private static final String NO_PASSWORD_ENCODER_MAPPED = "There is no password encoder mapped for the id '%s'. Check your configuration to ensure it matches one of the registered encoders."; + private static final String NO_PASSWORD_ENCODER_PREFIX = "Given that there is no default password encoder configured, each password must have a password encoding prefix. Please either prefix this password with '{noop}' or set a default password encoder in `SelectablePasswordEncoder`."; + private static final String MALFORMED_PASSWORD_ENCODER_PREFIX = "The name of the password encoder is improperly formatted or incomplete. The format should be '%sENCODER%spassword'."; + private final String idPrefix; + private final String idSuffix; + private final String idForEncode; + private final PasswordEncoder passwordEncoderForEncode; + private final Map idToPasswordEncoder; + private PasswordEncoder defaultPasswordEncoderForMatches; + + public SelectablePasswordEncoder(String idForEncode, Map idToPasswordEncoder) { + this(idForEncode, idToPasswordEncoder, "{", "}"); + } + + public SelectablePasswordEncoder(String idForEncode, Map idToPasswordEncoder, String idPrefix, String idSuffix) { + this.defaultPasswordEncoderForMatches = new UnmappedIdPasswordEncoder(); + if (idForEncode == null) { + throw new IllegalArgumentException("idForEncode cannot be null"); + } else if (idPrefix == null) { + throw new IllegalArgumentException("prefix cannot be null"); + } else if (idSuffix != null && !idSuffix.isEmpty()) { + if (idPrefix.contains(idSuffix)) { + throw new IllegalArgumentException("idPrefix " + idPrefix + " cannot contain idSuffix " + idSuffix); + } else if (!idToPasswordEncoder.containsKey(idForEncode)) { + throw new IllegalArgumentException("idForEncode " + idForEncode + "is not found in idToPasswordEncoder " + String.valueOf(idToPasswordEncoder)); + } else { + for(String id : idToPasswordEncoder.keySet()) { + if (id != null) { + if (!idPrefix.isEmpty() && id.contains(idPrefix)) { + throw new IllegalArgumentException("id " + id + " cannot contain " + idPrefix); + } + + if (id.contains(idSuffix)) { + throw new IllegalArgumentException("id " + id + " cannot contain " + idSuffix); + } + } + } + + this.idForEncode = idForEncode; + this.passwordEncoderForEncode = (PasswordEncoder)idToPasswordEncoder.get(idForEncode); + this.idToPasswordEncoder = new HashMap(idToPasswordEncoder); + this.idPrefix = idPrefix; + this.idSuffix = idSuffix; + } + } else { + throw new IllegalArgumentException("suffix cannot be empty"); + } + } + + public void setDefaultPasswordEncoderForMatches(PasswordEncoder defaultPasswordEncoderForMatches) { + if (defaultPasswordEncoderForMatches == null) { + throw new IllegalArgumentException("defaultPasswordEncoderForMatches cannot be null"); + } else { + this.defaultPasswordEncoderForMatches = defaultPasswordEncoderForMatches; + } + } + + public String encode(CharSequence rawPassword) { + String var10000 = this.idPrefix; + return var10000 + this.idForEncode + this.idSuffix + this.passwordEncoderForEncode.encode(rawPassword); + } + + public String encodeSelect(CharSequence rawPassword, String encoder) { + String var10000 = this.idPrefix;; + return var10000 + encoder + this.idSuffix + this.idToPasswordEncoder.get(encoder).encode(rawPassword); + } + + public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) { + if (rawPassword == null && prefixEncodedPassword == null) { + return true; + } else { + String id = this.extractId(prefixEncodedPassword); + PasswordEncoder delegate = (PasswordEncoder)this.idToPasswordEncoder.get(id); + if (delegate == null) { + return this.defaultPasswordEncoderForMatches.matches(rawPassword, prefixEncodedPassword); + } else { + String encodedPassword = this.extractEncodedPassword(prefixEncodedPassword); + return delegate.matches(rawPassword, encodedPassword); + } + } + } + + private String extractId(String prefixEncodedPassword) { + if (prefixEncodedPassword == null) { + return null; + } else { + int start = prefixEncodedPassword.indexOf(this.idPrefix); + if (start != 0) { + return null; + } else { + int end = prefixEncodedPassword.indexOf(this.idSuffix, start); + return end < 0 ? null : prefixEncodedPassword.substring(start + this.idPrefix.length(), end); + } + } + } + + public boolean upgradeEncoding(String prefixEncodedPassword) { + String id = this.extractId(prefixEncodedPassword); + if (!this.idForEncode.equalsIgnoreCase(id)) { + return true; + } else { + String encodedPassword = this.extractEncodedPassword(prefixEncodedPassword); + return ((PasswordEncoder)this.idToPasswordEncoder.get(id)).upgradeEncoding(encodedPassword); + } + } + + private String extractEncodedPassword(String prefixEncodedPassword) { + int start = prefixEncodedPassword.indexOf(this.idSuffix); + return prefixEncodedPassword.substring(start + this.idSuffix.length()); + } + + private class UnmappedIdPasswordEncoder implements PasswordEncoder { + public String encode(CharSequence rawPassword) { + throw new UnsupportedOperationException("encode is not supported"); + } + + public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) { + String id = SelectablePasswordEncoder.this.extractId(prefixEncodedPassword); + if (id != null && !id.isBlank()) { + throw new IllegalArgumentException(String.format("There is no password encoder mapped for the id '%s'. Check your configuration to ensure it matches one of the registered encoders.", id)); + } else { + if (prefixEncodedPassword != null && !prefixEncodedPassword.isBlank()) { + int start = prefixEncodedPassword.indexOf(SelectablePasswordEncoder.this.idPrefix); + int end = prefixEncodedPassword.indexOf(SelectablePasswordEncoder.this.idSuffix, start); + if (start < 0 && end < 0) { + throw new IllegalArgumentException("Given that there is no default password encoder configured, each password must have a password encoding prefix. Please either prefix this password with '{noop}' or set a default password encoder in `SelectablePasswordEncoder`."); + } + } + + throw new IllegalArgumentException(String.format("The name of the password encoder is improperly formatted or incomplete. The format should be '%sENCODER%spassword'.", SelectablePasswordEncoder.this.idPrefix, SelectablePasswordEncoder.this.idSuffix)); + } + } + } +} diff --git a/src/main/java/at/nanopenguin/fhtw/itse/pw_demo/UserDTO.java b/src/main/java/at/nanopenguin/fhtw/itse/pw_demo/UserDTO.java new file mode 100644 index 0000000..725e89d --- /dev/null +++ b/src/main/java/at/nanopenguin/fhtw/itse/pw_demo/UserDTO.java @@ -0,0 +1,12 @@ +package at.nanopenguin.fhtw.itse.pw_demo; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class UserDTO { + private String name; + private String password; + private String encoder; +} diff --git a/src/main/java/at/nanopenguin/fhtw/itse/pw_demo/WelcomeController.java b/src/main/java/at/nanopenguin/fhtw/itse/pw_demo/WelcomeController.java index 2bca5bc..14ddf8b 100644 --- a/src/main/java/at/nanopenguin/fhtw/itse/pw_demo/WelcomeController.java +++ b/src/main/java/at/nanopenguin/fhtw/itse/pw_demo/WelcomeController.java @@ -17,5 +17,4 @@ public class WelcomeController { return "welcome"; } - } diff --git a/src/main/resources/templates/register.html b/src/main/resources/templates/register.html new file mode 100644 index 0000000..dd3a5dd --- /dev/null +++ b/src/main/resources/templates/register.html @@ -0,0 +1,43 @@ + + + + Welcome! + + + + + Register User + + + + Name: + + + + + Password: + + + + + Password Encoder: + + -- Select Encoder -- + PBKDF2 + Unsalted SHA256 + MD5 + NoOp (Plain Text) + + + + + Register + + + + + + + + + \ No newline at end of file