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 com.nimbusds.jose.crypto.utils.ConstantTimeUtils;
030import com.nimbusds.jose.util.Base64URL;
031import net.jcip.annotations.Immutable;
032
033
034/**
035 * Secret. The secret value should be {@link #erase erased} when no longer in
036 * use.
037 */
038@Immutable
039public class Secret implements Serializable {
040        
041        
042        private static final long serialVersionUID = 1L;
043        
044        
045        /**
046         * The default byte length of generated secrets.
047         */
048        public static final int DEFAULT_BYTE_LENGTH = 32;
049        
050        
051        /**
052         * The secure random generator.
053         */
054        private static final SecureRandom SECURE_RANDOM = new SecureRandom();
055        
056        
057        /**
058         * The secret value.
059         */
060        private byte[] value;
061
062
063        /**
064         * Optional expiration date.
065         */
066        private final Date expDate;
067
068
069        /**
070         * Creates a new secret with the specified value.
071         *
072         * @param value The secret value. May be an empty string. Must be
073         *              UTF-8 encoded and not {@code null}.
074         */
075        public Secret(final String value) {
076
077                this(value, null);
078        }
079
080
081        /**
082         * Creates a new secret with the specified value and expiration date.
083         *
084         * @param value   The secret value. May be an empty string. Must be
085         *                UTF-8 encoded and not {@code null}.
086         * @param expDate The expiration date, {@code null} if not specified.
087         */
088        public Secret(final String value, final Date expDate) {
089
090                this.value = value.getBytes(StandardCharsets.UTF_8);
091                this.expDate = expDate;
092        }
093        
094        
095        /**
096         * Generates a new secret with a cryptographic random value of the
097         * specified byte length, Base64URL-encoded.
098         *
099         * @param byteLength The byte length of the secret value to generate. 
100         *                   Must be greater than one.
101         */
102        public Secret(final int byteLength) {
103
104                this(byteLength, null);
105        }
106
107
108        /**
109         * Generates a new secret with a cryptographic random value of the
110         * specified byte length, Base64URL-encoded, and the specified 
111         * expiration date.
112         *
113         * @param byteLength The byte length of the secret value to generate. 
114         *                   Must be greater than one.
115         * @param expDate    The expiration date, {@code null} if not 
116         *                   specified.
117         */
118        public Secret(final int byteLength, final Date expDate) {
119        
120                if (byteLength < 1)
121                        throw new IllegalArgumentException("The byte length must be a positive integer");
122                
123                byte[] n = new byte[byteLength];
124                
125                SECURE_RANDOM.nextBytes(n);
126
127                value = Base64URL.encode(n).toString().getBytes(StandardCharsets.UTF_8);
128                
129                this.expDate = expDate;
130        }
131        
132        
133        /**
134         * Generates a new secret with a cryptographic 256-bit (32-byte) random
135         * value, Base64URL-encoded.
136         */
137        public Secret() {
138
139                this(DEFAULT_BYTE_LENGTH);
140        }
141
142
143        /**
144         * Gets the value of this secret.
145         *
146         * @return The value as a UTF-8 encoded string, {@code null} if it has 
147         *         been erased.
148         */
149        public String getValue() {
150
151                if (value == null) {
152                        return null; // value has been erased
153                }
154
155                return new String(value, StandardCharsets.UTF_8);
156        }
157        
158        
159        /**
160         * Gets the value of this secret.
161         *
162         * @return The value as a byte array, {@code null} if it has 
163         *         been erased.
164         */
165        public byte[] getValueBytes() {
166
167                return value;
168        }
169        
170        
171        /**
172         * Gets the SHA-256 hash of this secret.
173         *
174         * @return The SHA-256 hash, {@code null} if the secret value has been
175         *         erased.
176         */
177        public byte[] getSHA256() {
178                
179                if (value == null) {
180                        return null;
181                }
182                
183                try {
184                        MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
185                        return sha256.digest(value);
186                } catch (NoSuchAlgorithmException e) {
187                        throw new RuntimeException(e);
188                }
189        }
190
191
192        /**
193         * Erases of the value of this secret.
194         */
195        public void erase() {
196
197                if (value == null) {
198                        return; // Already erased
199                }
200                
201                Arrays.fill(value, (byte) 0);
202                
203                value = null;
204        }
205
206
207        /**
208         * Gets the expiration date of this secret.
209         *
210         * @return The expiration date, {@code null} if not specified.
211         */
212        public Date getExpirationDate() {
213
214                return expDate;
215        }
216
217
218        /**
219         * Checks is this secret has expired.
220         *
221         * @return {@code true} if the secret has an associated expiration date
222         *         which is in the past (according to the current system time), 
223         *         else returns {@code false}.
224         */
225        public boolean expired() {
226
227                if (expDate == null) {
228                        return false; // never expires
229                }
230
231                final Date now = new Date();
232
233                return expDate.before(now);
234        }
235        
236        
237        /**
238         * Constant time comparison of the SHA-256 hashes of this and another
239         * secret.
240         *
241         * @param other The other secret. May be {@code null}.
242         *
243         * @return {@code true} if the SHA-256 hashes of the two secrets are
244         *         equal, else {@code false}.
245         */
246        public boolean equalsSHA256Based(final Secret other) {
247                
248                if (other == null) {
249                        return false;
250                }
251                
252                byte[] thisHash = getSHA256();
253                byte[] otherHash = other.getSHA256();
254                
255                if (thisHash == null || otherHash == null) {
256                        return false;
257                }
258                
259                return ConstantTimeUtils.areEqual(thisHash, otherHash);
260        }
261        
262        
263        /**
264         * Comparison with another secret is constant time.
265         *
266         * @param o The other object. May be {@code null}.
267         *
268         * @return {@code true} if both objects are equal, else {@code false}.
269         */
270        @Override
271        public boolean equals(final Object o) {
272                if (this == o) return true;
273                if (value == null) return false;
274                if (!(o instanceof Secret)) return false;
275                Secret secret = (Secret) o;
276                return ConstantTimeUtils.areEqual(value, secret.value);
277        }
278
279
280        @Override
281        public int hashCode() {
282                return Arrays.hashCode(value);
283        }
284}