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