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}