🚀 Part 2: Why I Chose char[] for Passwords — And What I Learned

Welcome back to the Spring Security series! 👋 In Chapter 1, we set up Spring Security with SecurityFilterChain, CSRF, and session management. Now, let’s talk about a subtle but important detail: how we handle passwords in memory.

🔗 GitHub Repo: https://github.com/manueltechlabs/java-blog-project/tree/main

🧠 The Rationale: Why char[] Makes Sense

Java has long recommended using char[] over String for passwords — and there’s good reason:

AdvantageExplanation
🔁 MutabilityYou can overwrite a char[] with Arrays.fill(pwd, '0'), reducing memory exposure.
🚫 No String Pool RiskString objects live in the heap and may persist until garbage collection — dangerous if someone takes a memory dump.
📵 Safer LoggingPrinting a char[] shows [C@hash, not the actual password — helpful if logs are exposed.

Even JPasswordField.getPassword() returns char[] — Java itself trusts this pattern1.

🔍 My Implementation

Here’s how I used it in the Password entity:

java
1234567891011121314151617181920212223
      @Entity
public class Password {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long passwordId;

    @NotEmpty(message = "Password missing")
    private char[] word;

    private String resetToken;
    private LocalDateTime resetTokenExpiry;

    public Password(char[] word) {
        this.word = word;
    }

    // Clear password from memory after use
    public void clear() {
        if (this.word != null) {
            Arrays.fill(this.word, '\0');
        }
    }
}
    

In the controller:

java
12345678910111213141516171819202122232425262728293031323334353637383940
      @Controller
public class AccountController {
    
    @Autowired
    AccountService accountService;

    @Autowired
    PasswordService passwordService;

@PostMapping("/register")
    public String registerPost(
        @RequestParam("file") MultipartFile file,
        @Valid @ModelAttribute Account account,
        BindingResult resultAccount,
        @Valid @ModelAttribute Password password,
        BindingResult resultPassword        
    ) {
        // check that email is not already taken
        Optional<Account> optionalAccount = accountService.findOneByEmail(account.getEmail());
        if(optionalAccount.isPresent()) {
            resultAccount.rejectValue("email","error.email", "This email address is already in use");            
        }

        // checking both resultAccount or resultPassword don't have errors
        if (resultAccount.hasErrors() | resultPassword.hasErrors()) {

            return "accountView/register";
        }

        if(!file.isEmpty()) {
            System.out.println("file: " + file);
            account.setProfilePicture(FileStorage.save(file, "userImg"));
            FileStorage.waitt();
        }
        passwordService.save(password);
        account.setPassword(password);
        accountService.save(account);
        return "redirect:/";
    }
}
    

And in service logic:

java
12345678910111213141516171819202122232425262728293031
      @Service("passwordService")
public class PaswordServiceImpl implements PasswordService {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    PasswordRepository passwordRepository;

    @Override
    public void save(Password password) {
        try {
            password.setWord(passwordEncoder.encode(CharBuffer.wrap(password.getWord())).toCharArray());
            passwordRepository.save(password);
        } finally {
            password.clear();
        }
    }
    @Override
    public boolean checkPassword(Password password, char[] keyWord) {
        try {
            return passwordEncoder.matches(String.valueOf(keyWord), String.valueOf(password.getWord()));
        } finally {
            if(keyWord != null) {
                Arrays.fill(keyWord, '\0');
                password.clear();
            }
        }        
    }
    
}
    

⚠️ Framework Limitations

Despite best intentions, Spring Security doesn’t fully support char[].

As reported in spring-projects/spring-security#17830:

“From a security perspective, it is recommended to use char[] to avoid leaving password values in immutable String objects in memory.” 2

But:

  • UsernamePasswordAuthenticationToken expects String or CharSequence.
  • Form data binding often creates String before your code runs.

So while char[] helps within your domain, the framework ecosystem limits its full benefit.

✅ What Still Matters

Even with these constraints:

  • ✅ I control when and where the password lives.
  • ✅ I can clear it explicitly after hashing.
  • ✅ I reduce accidental logging risks.

It’s not perfect — but it’s better than doing nothing.

👉 Next Up: Chapter 3 — User Registration & Default Roles We’ll walk through how new users get auto-assigned ROLE_SUBSCRIBER, complete with registration timestamp and default profile picture — all securely handled.

Stay tuned!

🔗 GitHub Repo: https://github.com/manueltechlabs/java-blog-project/tree/main

Footnotes

  1. https://stackoverflow.com/questions/8881291/why-is-char-preferred-over-string-for-passwords

  2. https://github.com/spring-projects/spring-security/issues/17830