001// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
002//
003// SPDX-License-Identifier: Apache-2.0
004
005package org.pgpainless.util;
006
007import javax.annotation.Nonnull;
008import javax.annotation.Nullable;
009import java.util.Arrays;
010
011import static org.pgpainless.util.BCUtil.constantTimeAreEqual;
012
013public class Passphrase {
014
015    public final Object lock = new Object();
016
017    private final char[] chars;
018    private boolean valid = true;
019
020    /**
021     * Passphrase for keys etc.
022     *
023     * @param chars may be null for empty passwords.
024     */
025    public Passphrase(@Nullable char[] chars) {
026        if (chars == null) {
027            this.chars = null;
028        } else {
029            char[] trimmed = removeTrailingAndLeadingWhitespace(chars);
030            if (trimmed.length == 0) {
031                this.chars = null;
032            } else {
033                this.chars = trimmed;
034            }
035        }
036    }
037
038    /**
039     * Return a copy of the passed in char array, with leading and trailing whitespace characters removed.
040     *
041     * @param chars char array
042     * @return copy of char array with leading and trailing whitespace characters removed
043     */
044    private static char[] removeTrailingAndLeadingWhitespace(char[] chars) {
045        int i = 0;
046        while (i < chars.length && isWhitespace(chars[i])) {
047            i++;
048        }
049        int j = chars.length - 1;
050        while (j >= i && isWhitespace(chars[j])) {
051            j--;
052        }
053
054        char[] trimmed = new char[chars.length - i - (chars.length - 1 - j)];
055        System.arraycopy(chars, i, trimmed, 0, trimmed.length);
056
057        return trimmed;
058    }
059
060    /**
061     * Return true, if the passed in char is a whitespace symbol (space, newline, tab).
062     *
063     * @param xar char
064     * @return true if whitespace
065     */
066    private static boolean isWhitespace(char xar) {
067        return xar == ' ' || xar == '\n' || xar == '\t';
068    }
069
070    /**
071     * Create a {@link Passphrase} from a {@link String}.
072     *
073     * @param password password
074     * @return passphrase
075     */
076    public static Passphrase fromPassword(@Nonnull String password) {
077        return new Passphrase(password.toCharArray());
078    }
079
080    /**
081     * Overwrite the char array with spaces and mark the {@link Passphrase} as invalidated.
082     */
083    public void clear() {
084        synchronized (lock) {
085            if (chars != null) {
086                Arrays.fill(chars, ' ');
087            }
088            valid = false;
089        }
090    }
091
092    /**
093     * Return a copy of the underlying char array.
094     * A return value of {@code null} represents no password.
095     *
096     * @return passphrase chars.
097     *
098     * @throws IllegalStateException in case the password has been cleared at this point.
099     */
100    public @Nullable char[] getChars() {
101        synchronized (lock) {
102            if (!valid) {
103                throw new IllegalStateException("Passphrase has been cleared.");
104            }
105
106            if (chars == null) {
107                return null;
108            }
109
110            char[] copy = new char[chars.length];
111            System.arraycopy(chars, 0, copy, 0, chars.length);
112            return copy;
113        }
114    }
115
116    /**
117     * Return true if the passphrase has not yet been cleared.
118     *
119     * @return valid
120     */
121    public boolean isValid() {
122        synchronized (lock) {
123            return valid;
124        }
125    }
126
127    /**
128     * Return true if the passphrase represents no password.
129     *
130     * @return empty
131     */
132    public boolean isEmpty() {
133        synchronized (lock) {
134            return valid && chars == null;
135        }
136    }
137
138    /**
139     * Represents a {@link Passphrase} instance that represents no password.
140     *
141     * @return empty passphrase
142     */
143    public static Passphrase emptyPassphrase() {
144        return new Passphrase(null);
145    }
146
147    @Override
148    public int hashCode() {
149        if (getChars() == null) {
150            return 0;
151        }
152        return new String(getChars()).hashCode();
153    }
154
155    @Override
156    public boolean equals(Object obj) {
157        if (obj == null) {
158            return false;
159        }
160        if (this == obj) {
161            return true;
162        }
163        if (!(obj instanceof Passphrase)) {
164            return false;
165        }
166        Passphrase other = (Passphrase) obj;
167        return (getChars() == null && other.getChars() == null) ||
168                constantTimeAreEqual(getChars(), other.getChars());
169    }
170}