Skip to content

Feature/password hashing #34

Merged
merged 6 commits into from
Mar 3, 2026
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
6 changes: 6 additions & 0 deletions helpmehelpapplication/helpmehelpapplication.iml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<module version="4">
<component name="CheckStyle-IDEA-Module" serialisationVersion="2">
<option name="activeLocationsIds" />
</component>
</module>
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package ntnu.sytemutvikling.team6.models;

import ntnu.sytemutvikling.team6.models.user.User;

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

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package ntnu.sytemutvikling.team6.models;

import ntnu.sytemutvikling.team6.models.user.User;

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

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package ntnu.sytemutvikling.team6.models;
package ntnu.sytemutvikling.team6.models.user;

import java.util.*;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package ntnu.sytemutvikling.team6.models;
package ntnu.sytemutvikling.team6.models.user;

/**
* Supported application languages.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package ntnu.sytemutvikling.team6.models;
package ntnu.sytemutvikling.team6.models.user;

import java.time.LocalDateTime;
import java.util.UUID;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package ntnu.sytemutvikling.team6.models;
package ntnu.sytemutvikling.team6.models.user;

/**
* Available users
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package ntnu.sytemutvikling.team6.models;
package ntnu.sytemutvikling.team6.models.user;

// Mangler Enhetstesting

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package ntnu.sytemutvikling.team6.models.user;

import ntnu.sytemutvikling.team6.security.PasswordHasher;

import java.util.UUID;

// TODO: Enhetstesting mangler

/**
* Represents a user.
*
* @author Robin Strand Prestmo
*/
public class User {
private static final PasswordHasher passwordHasher = new PasswordHasher();

private final UUID id;
private String name;
private String email;
private String passwordHash;
private final Role role;
private final Settings settings;
private final Inbox inbox;

/**
* Creates a new user
*
* @param id gives the user a unique identifier with UUID
* @param name the name of the user
* @param email the email of the user
* @param password the password for the user
* @param role users role
* @param settings the user´s settings
* @param inbox the user´s inbox
*
*/
public User(UUID id,
String name,
String email,
String password,
Role role,
Settings settings,
Inbox inbox) {
if (id == null) {
throw new IllegalArgumentException("ID cannot be null.");
}

if (name == null || name.isBlank()) {
throw new IllegalArgumentException("Name cannot be null or blank.");
}

if (email == null || email.isBlank() || !email.contains("@") || !email.contains(".")) {
throw new IllegalArgumentException("Email cannot be null or blank, and must contain '@' and '.'");
}

if (role == null) {
throw new IllegalArgumentException("Role cannot be null");
}

if (settings == null) {
throw new IllegalArgumentException("Settings cannot be null");
}

if (inbox == null) {
throw new IllegalArgumentException("Inbox cannot be null");
}

this.id = id;
this.name = name;
this.email = email;
this.passwordHash = passwordHasher.getHashPassword(password);
this.role = role;
this.settings = settings;
this.inbox = inbox;
}

// Add Getters

public UUID getId() {
return id;
}

public String getName() {
return name;
}

public String getEmail() {
return email;
}

public Role getRole() {
return role;
}

public Settings getSettings() {
return settings;
}

public Inbox getInbox() {
return inbox;
}

// Add Setters

public void setName(String name) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("Name cannot be null or blank.");
}
this.name = name;
}

public void setPassword(String password) {
this.passwordHash = passwordHasher.getHashPassword(password);
}

public void setEmail(String email) {
if (email == null || email.isBlank() || !email.contains("@") || !email.contains(".")) {
throw new IllegalArgumentException("Email cannot be null or blank, and must contains '@' and '.'");
}
this.email = email;
}

// Other methods

public boolean checkPassword(String password) {
return passwordHasher.isValidPassword(password, passwordHash);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package ntnu.sytemutvikling.team6.security;

import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Base64;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;

/**
* A utility for hashing and verifying passwords using PBKDF2.
*
* <p>The generated hash contains both a random salt and the hashed password,
* encoded as Base64 string.
* </p>
*
* @author Robin Strand Prestmo
*/
public final class PasswordHasher {
private static final SecureRandom RNG = new SecureRandom();

/**
* Hashes a password using PBKDF2 and a random salt.
*
* @param password the password to hash.
* @return a Base64 string containing the salt and the hashed password.
* @throws IllegalArgumentException if the password is null or blank.
*/
public String getHashPassword(String password) {
if (password == null || password.isBlank()) {
throw new IllegalArgumentException("Password cannot be null or blank.");
}

String hashPass = "";

try {
// 1. Create salt
byte[] salt = new byte[16];
RNG.nextBytes(salt);

// 2. Create PBKDF2 Hash value
PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, 100000, 32 * 8);
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
byte[] hash = factory.generateSecret(spec).getEncoded();

// 3. Combine salt and password bytes
byte[] hashBytes = new byte[48];
System.arraycopy(salt, 0, hashBytes, 0, 16);
System.arraycopy(hash, 0, hashBytes, 16, 32);

// 4. Turn the combined salt+hash into a string.
hashPass = Base64.getEncoder().encodeToString(hashBytes);
} catch (Exception e) {
throw new RuntimeException("Error while hashing password.", e);
}
return hashPass;
}

/**
* Checks if the password matches a perviously stored hash.
*
* @param password The password the user types.
* @param hashPass Is the stored hashed password
* @return True if password is valid, otherwise false.
*/
public boolean isValidPassword(String password, String hashPass) {
if (password == null || password.isBlank()) {
return false;
}

try {
// Extract the bytes
byte[] hashBytes = Base64.getDecoder().decode(hashPass);

// Get salt
byte[] salt = new byte[16];
System.arraycopy(hashBytes, 0, salt, 0, 16);

// Compute the hash on the password the user entered
PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, 100000, 32 * 8);
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
byte[] hash = factory.generateSecret(spec).getEncoded();

// Compare results
byte[] storedHash = new byte[32];
System.arraycopy(hashBytes, 16, storedHash, 0, 32);

return MessageDigest.isEqual(storedHash, hash);

} catch (Exception e) {
throw new RuntimeException("Error while validating password.", e);
}
}
}
Loading