001/*
002 * This file is part of the Keycloak Moodle authenticator
003 * Copyright (C) 2024 Michael N. Lipp
004 *
005 * This program is free software; you can redistribute it and/or modify it 
006 * under the terms of the GNU Lesser General Public License as published
007 * by the Free Software Foundation; either version 3 of the License, or 
008 * (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful, but 
011 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
012 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 
013 * License for more details.
014 *
015 * You should have received a copy of the GNU Lesser General Public License along 
016 * with this program; if not, see <http://www.gnu.org/licenses/>.
017 */
018
019package org.jdrupes.keycloak.moodleauth.moodle.service;
020
021import java.lang.ref.Reference;
022import java.lang.ref.ReferenceQueue;
023import java.lang.ref.WeakReference;
024import java.util.Arrays;
025import java.util.Optional;
026
027/**
028 * Stores a password in such a way that it can be cleared. Automatically 
029 * clears the storage if an object of this type becomes weakly reachable.
030 */
031public class Password {
032
033    private static ReferenceQueue<Password> toBeCleared
034        = new ReferenceQueue<>();
035    private static Thread purger;
036    private static final char[] EMPTY_PASSWORD = new char[0];
037
038    @SuppressWarnings("PMD.AvoidFieldNameMatchingTypeName")
039    private char[] password;
040    @SuppressWarnings({ "PMD.SingularField", "unused" })
041    private final WeakReference<Password> passwordRef;
042
043    /**
044     * Instantiates a new password representation.
045     *
046     * @param password the password
047     */
048    @SuppressWarnings({ "PMD.UseVarargs", "PMD.AssignmentToNonFinalStatic",
049        "PMD.ArrayIsStoredDirectly" })
050    public Password(char[] password) {
051        synchronized (Password.class) {
052            if (purger == null) {
053                purger = new Thread(() -> {
054                    while (true) {
055                        try {
056                            Reference<? extends Password> passwordRef
057                                = toBeCleared.remove();
058                            Optional.ofNullable(passwordRef.get())
059                                .ifPresent(Password::clear);
060                            passwordRef.clear();
061                        } catch (InterruptedException e) {
062                            break;
063                        }
064                    }
065                });
066                purger.setName("PasswordPurger");
067                purger.setDaemon(true);
068                purger.start();
069            }
070        }
071        this.password = password;
072        passwordRef = new WeakReference<>(this, toBeCleared);
073    }
074
075    /**
076     * Clear the stored password.
077     */
078    public void clear() {
079        for (int i = 0; i < password.length; i++) {
080            password[i] = 0;
081        }
082        // Don't even remember its length.
083        password = EMPTY_PASSWORD;
084    }
085
086    /**
087     * Returns the stored password. This is returns a reference to the
088     * internally used array.
089     *
090     * @return the char[]
091     */
092    @SuppressWarnings("PMD.MethodReturnsInternalArray")
093    public char[] password() {
094        return password;
095    }
096
097    /**
098     * Compare to a given string.
099     *
100     * @param value the value to compare to
101     * @return true, if successful
102     */
103    @SuppressWarnings("PMD.SimplifyBooleanReturns")
104    public boolean compareTo(String value) {
105        if (value == null) {
106            return false;
107        }
108        return Arrays.equals(value.toCharArray(), password);
109    }
110
111    /**
112     * Compare to a given char array.
113     *
114     * @param value the value to compare to
115     * @return true, if successful
116     */
117    @SuppressWarnings({ "PMD.UseVarargs", "PMD.SimplifyBooleanReturns" })
118    public boolean compareTo(char[] value) {
119        if (value == null) {
120            return false;
121        }
122        return Arrays.equals(value, password);
123    }
124
125    @Override
126    @SuppressWarnings("PMD.SimplifyBooleanReturns")
127    public boolean equals(Object other) {
128        if (!(other instanceof Password)) {
129            return false;
130        }
131        return compareTo(((Password) other).password);
132    }
133
134    /**
135     * Passwords shouldn't be used in sets or as keys. To avoid
136     * disclosing any information about the password, this method
137     * always returns 0.
138     *
139     * @return 0
140     */
141    @Override
142    public int hashCode() {
143        return 0;
144    }
145
146    /**
147     * Return "`(hidden)`". Should prevent the password from appearing
148     * unintentionally in outputs.
149     *
150     * @return the string
151     */
152    @Override
153    public String toString() {
154        return "(hidden)";
155    }
156}