001/*
002 * oauth2-oidc-sdk
003 *
004 * Copyright 2012-2016, Connect2id Ltd and contributors.
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use
007 * this file except in compliance with the License. You may obtain a copy of the
008 * License at
009 *
010 *    http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software distributed
013 * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
014 * CONDITIONS OF ANY KIND, either express or implied. See the License for the
015 * specific language governing permissions and limitations under the License.
016 */
017
018package com.nimbusds.oauth2.sdk.auth;
019
020
021import java.io.Serializable;
022import java.nio.charset.StandardCharsets;
023import java.security.MessageDigest;
024import java.security.NoSuchAlgorithmException;
025import java.security.SecureRandom;
026import java.util.Arrays;
027import java.util.Date;
028
029import net.jcip.annotations.Immutable;
030
031import com.nimbusds.jose.crypto.utils.ConstantTimeUtils;
032import com.nimbusds.jose.util.Base64URL;
033
034
035/**
036 * Secret. The secret value should be {@link #erase erased} when no longer in
037 * use.
038 */
039@Immutable
040public class Secret implements Serializable {
041        
042        
043        private static final long serialVersionUID = 1L;
044        
045        
046        /**
047         * The default byte length of generated secrets.
048         */
049        public static final int DEFAULT_BYTE_LENGTH = 32;
050        
051        
052        /**
053         * The secure random generator.
054         */
055        private static final SecureRandom SECURE_RANDOM = new SecureRandom();
056        
057        
058        /**
059         * The secret value.
060         */
061        private byte[] value;
062
063
064        /**
065         * Optional expiration date.
066         */
067        private final Date expDate;
068
069
070        /**
071         * Creates a new secret with the specified value.
072         *
073         * @param value The secret value. May be an empty string. Must be
074         *              UTF-8 encoded and not {@code null}.
075         */
076        public Secret(final String value) {
077
078                this(value, null);
079        }
080
081
082        /**
083         * Creates a new secret with the specified value and expiration date.
084         *
085         * @param value   The secret value. May be an empty string. Must be
086         *                UTF-8 encoded and not {@code null}.
087         * @param expDate The expiration date, {@code null} if not specified.
088         */
089        public Secret(final String value, final Date expDate) {
090
091                this.value = value.getBytes(StandardCharsets.UTF_8);
092                this.expDate = expDate;
093        }
094        
095        
096        /**
097         * Generates a new secret with a cryptographic random value of the
098         * specified byte length, Base64URL-encoded.
099         *
100         * @param byteLength The byte length of the secret value to generate. 
101         *                   Must be greater than one.
102         */
103        public Secret(final int byteLength) {
104
105                this(byteLength, null);
106        }
107
108
109        /**
110         * Generates a new secret with a cryptographic random value of the
111         * specified byte length, Base64URL-encoded, and the specified 
112         * expiration date.
113         *
114         * @param byteLength The byte length of the secret value to generate. 
115         *                   Must be greater than one.
116         * @param expDate    The expiration date, {@code null} if not 
117         *                   specified.
118         */
119        public Secret(final int byteLength, final Date expDate) {
120        
121                if (byteLength < 1)
122                        throw new IllegalArgumentException("The byte length must be a positive integer");
123                
124                byte[] n = new byte[byteLength];
125                
126                SECURE_RANDOM.nextBytes(n);
127
128                value = Base64URL.encode(n).toString().getBytes(StandardCharsets.UTF_8);
129                
130                this.expDate = expDate;
131        }
132        
133        
134        /**
135         * Generates a new secret with a cryptographic 256-bit (32-byte) random
136         * value, Base64URL-encoded.
137         */
138        public Secret() {
139
140                this(DEFAULT_BYTE_LENGTH);
141        }
142
143
144        /**
145         * Gets the value of this secret.
146         *
147         * @return The value as a UTF-8 encoded string, {@code null} if it has 
148         *         been erased.
149         */
150        public String getValue() {
151
152                if (value == null) {
153                        return null; // value has been erased
154                }
155
156                return new String(value, StandardCharsets.UTF_8);
157        }
158        
159        
160        /**
161         * Gets the value of this secret.
162         *
163         * @return The value as a byte array, {@code null} if it has 
164         *         been erased.
165         */
166        public byte[] getValueBytes() {
167
168                return value;
169        }
170        
171        
172        /**
173         * Gets the SHA-256 hash of this secret.
174         *
175         * @return The SHA-256 hash, {@code null} if the secret value has been
176         *         erased.
177         */
178        public byte[] getSHA256() {
179                
180                if (value == null) {
181                        return null;
182                }
183                
184                try {
185                        MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
186                        return sha256.digest(value);
187                } catch (NoSuchAlgorithmException e) {
188                        throw new RuntimeException(e);
189                }
190        }
191
192
193        /**
194         * Erases of the value of this secret.
195         */
196        public void erase() {
197
198                if (value == null) {
199                        return; // Already erased
200                }
201                
202                Arrays.fill(value, (byte) 0);
203                
204                value = null;
205        }
206
207
208        /**
209         * Gets the expiration date of this secret.
210         *
211         * @return The expiration date, {@code null} if not specified.
212         */
213        public Date getExpirationDate() {
214
215                return expDate;
216        }
217
218
219        /**
220         * Checks is this secret has expired.
221         *
222         * @return {@code true} if the secret has an associated expiration date
223         *         which is in the past (according to the current system time), 
224         *         else returns {@code false}.
225         */
226        public boolean expired() {
227
228                if (expDate == null) {
229                        return false; // never expires
230                }
231
232                final Date now = new Date();
233
234                return expDate.before(now);
235        }
236        
237        
238        /**
239         * Constant time comparison of the SHA-256 hashes of this and another
240         * secret.
241         *
242         * @param other The other secret. May be {@code null}.
243         *
244         * @return {@code true} if the SHA-256 hashes of the two secrets are
245         *         equal, {@code false} if the hashes don't match or the secret
246         *         values are {@link #erase() erased}.
247         */
248        @Deprecated
249        public boolean equalsSHA256Based(final Secret other) {
250                
251                if (other == null) {
252                        return false;
253                }
254                
255                byte[] thisHash = getSHA256();
256                byte[] otherHash = other.getSHA256();
257                
258                if (thisHash == null || otherHash == null) {
259                        return false;
260                }
261                
262                return ConstantTimeUtils.areEqual(thisHash, otherHash);
263        }
264        
265        
266        /**
267         * Comparison with another secret is constant time, based on the
268         * secrets' {@link #getSHA256() SHA-256 hashes}.
269         *
270         * @param o The other object. May be {@code null}.
271         *
272         * @return {@code true} if both objects are equal, else {@code false}.
273         */
274        @Override
275        public boolean equals(final Object o) {
276                if (this == o) return true;
277                if (value == null) return false;
278                if (!(o instanceof Secret)) return false;
279                Secret otherSecret = (Secret) o;
280                return equalsSHA256Based(otherSecret);
281        }
282
283
284        @Override
285        public int hashCode() {
286                return Arrays.hashCode(value);
287        }
288}