001/* 002 * ============================================================================ 003 * Copyright © 2002-2024 by Thomas Thrien. 004 * All Rights Reserved. 005 * ============================================================================ 006 * Licensed to the public under the agreements of the GNU Lesser General Public 007 * License, version 3.0 (the "License"). You may obtain a copy of the License at 008 * 009 * http://www.gnu.org/licenses/lgpl.html 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 013 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 014 * License for the specific language governing permissions and limitations 015 * under the License. 016 */ 017 018package org.tquadrat.foundation.util; 019 020import static java.lang.String.format; 021import static org.apiguardian.api.API.Status.STABLE; 022import static org.tquadrat.foundation.lang.CommonConstants.UTF8; 023import static org.tquadrat.foundation.lang.Objects.nonNull; 024import static org.tquadrat.foundation.lang.Objects.requireNonNullArgument; 025import static org.tquadrat.foundation.util.IOUtils.determineCheckSum; 026 027import java.io.File; 028import java.io.IOException; 029import java.math.BigInteger; 030import java.security.MessageDigest; 031import java.security.NoSuchAlgorithmException; 032import java.util.HexFormat; 033import java.util.Locale; 034 035import org.apiguardian.api.API; 036import org.tquadrat.foundation.annotation.ClassVersion; 037import org.tquadrat.foundation.annotation.UtilityClass; 038import org.tquadrat.foundation.exception.ImpossibleExceptionError; 039import org.tquadrat.foundation.exception.PrivateConstructorForStaticClassCalledError; 040 041/** 042 * This class provides some utility functions that are helpful in the 043 * security arena. <br> 044 * <br>The methods are thread safe, but they use a global message digest. As 045 * a consequence, multiple threads that are calculating hashes will serialise 046 * on the use of those digest. That is acceptable for an application where 047 * this calculation does not occur that often (for example a web application 048 * that needs to check a password at login) but not for an application that 049 * uses multiple threads to calculate the hashes for a bunch of files. 050 * 051 * @extauthor Thomas Thrien - thomas.thrien@tquadrat.org 052 * @version $Id: SecurityUtils.java 1086 2024-01-05 23:18:33Z tquadrat $ 053 * @since 0.0.5 054 * 055 * @UMLGraph.link 056 */ 057@ClassVersion( sourceVersion = "$Id: SecurityUtils.java 1086 2024-01-05 23:18:33Z tquadrat $" ) 058@UtilityClass 059public final class SecurityUtils 060{ 061 /*-----------*\ 062 ====** Constants **======================================================== 063 \*-----------*/ 064 /** 065 * A recommended prime for the Diffie-Hellman-Merkle key exchange scheme. 066 * 067 * @see #calculateDiffieHellmanEncryptionKey(BigInteger,BigInteger,BigInteger) 068 * @see #calculateDiffieHellmanPublicValue(BigInteger,BigInteger,BigInteger) 069 * @see <a href="http://tools.ietf.org/html/rfc2412#page-45">http://tools.ietf.org/html/rfc2412#page-45</a> 070 */ 071 @API( status = STABLE, since = "0.0.5" ) 072 public static final BigInteger DHM_PRIME = new BigInteger 073 ( 074 """ 075 2410312426921032588552076022197566074856950548502459942654116941958\ 076 1088316826122288900938582613416146732271414779040121965036489570505\ 077 8263194273070680500922306273474534107340669624601458936165977404102\ 078 7169249453200378729434170325843778659198143763193776859869524088940\ 079 1955773461198435453015470437472077499697637500843089263392955599688\ 080 8245787241299381012913029459299994792636526405928464720973038494721\ 081 1681434464714438488520940127459844288859336526896320919633919""" ); 082 083 /** 084 * A recommended prime modulus (primitive root) for the 085 * Diffie-Hellman-Merkle key exchange scheme. 086 * 087 * @see #calculateDiffieHellmanEncryptionKey(BigInteger,BigInteger,BigInteger) 088 * @see #calculateDiffieHellmanPublicValue(BigInteger,BigInteger,BigInteger) 089 * @see <a href="http://tools.ietf.org/html/rfc2412#page-45">http://tools.ietf.org/html/rfc2412#page-45</a> 090 */ 091 @API( status = STABLE, since = "0.0.5" ) 092 public static final BigInteger DHM_PRIME_MOD = BigInteger.valueOf( 21 ); 093 094 /** 095 * The message that indicates that the named algorithm is not supported: 096 * {@value}. 097 */ 098 private static final String MSG_AlgorithmNotSupported = "MessageDigest does not support '%1$s' Algorithm"; 099 100 /** 101 * The length for an MD5 hash: {@value}. 102 */ 103 @API( status = STABLE, since = "0.0.5" ) 104 public static final int MD5HASH_Length = 32; 105 106 /** 107 * The length for an SHA1 hash: {@value}. 108 */ 109 @API( status = STABLE, since = "0.0.8" ) 110 public static final int SHA1HASH_Length = 40; 111 112 /** 113 * The length for an SHA256 hash: {@value}. 114 */ 115 @API( status = STABLE, since = "0.0.8" ) 116 public static final int SHA256HASH_Length = 64; 117 118 /*------------------------*\ 119 ====** Static Initialisations **=========================================== 120 \*------------------------*/ 121 /** 122 * The message digest that is used to encrypt the passwords with the MD5 123 * hash algorithm. 124 */ 125 private static final MessageDigest m_MD5MessageDigest; 126 127 /** 128 * The message digest that is used to encrypt the passwords with the SHA-1 129 * hash algorithm. 130 */ 131 private static final MessageDigest m_SHA1MessageDigest; 132 133 /** 134 * The message digest that is used to encrypt the passwords with the SHA-1 135 * hash algorithm. 136 */ 137 private static final MessageDigest m_SHA256MessageDigest; 138 139 static 140 { 141 //---* Create the MD5 digest *----------------------------------------- 142 var algorithm = "MD5"; 143 try 144 { 145 m_MD5MessageDigest = MessageDigest.getInstance( algorithm ); 146 } 147 catch( final NoSuchAlgorithmException e ) 148 { 149 throw new ImpossibleExceptionError( format( MSG_AlgorithmNotSupported, algorithm ), e ); 150 } 151 152 //---* Create the SHA digest *----------------------------------------- 153 algorithm = "SHA"; 154 try 155 { 156 m_SHA1MessageDigest = MessageDigest.getInstance( algorithm ); 157 } 158 catch( final NoSuchAlgorithmException e ) 159 { 160 throw new ImpossibleExceptionError( format( MSG_AlgorithmNotSupported, algorithm ), e ); 161 } 162 163 //---* Create the SHA-256 digest *------------------------------------- 164 algorithm = "SHA-256"; 165 try 166 { 167 m_SHA256MessageDigest = MessageDigest.getInstance( algorithm ); 168 } 169 catch( final NoSuchAlgorithmException e ) 170 { 171 throw new ImpossibleExceptionError( format( MSG_AlgorithmNotSupported, algorithm ), e ); 172 } 173 } 174 175 /*--------------*\ 176 ====** Constructors **===================================================== 177 \*--------------*/ 178 /** 179 * No instances are allowed for this class. 180 */ 181 private SecurityUtils() { throw new PrivateConstructorForStaticClassCalledError( SecurityUtils.class ); } 182 183 /*---------*\ 184 ====** Methods **========================================================== 185 \*---------*/ 186 /** 187 * <p>{@summary Performs the calculation for the Diffie-Hellmann-Merkle 188 * key exchange procedure.}</p> 189 * <p>From Wikipedia:</p> <blockquote><p>Diffie–Hellman establishes a shared 190 * secret that can be used for secret communications while exchanging data 191 * over a public network. Diffie–Hellman key exchange (D-H) is a specific 192 * method of exchanging cryptographic keys. It is one of the earliest 193 * practical examples of key exchange implemented within the field of 194 * cryptography. The Diffie–Hellman key exchange method allows two parties 195 * that have no prior knowledge of each other to jointly establish a 196 * shared secret key over an insecure communications channel. This key can 197 * then be used to encrypt subsequent communications using a symmetric key 198 * cipher.</p> 199 * <p>The scheme was first published by Whitfield Diffie and Martin 200 * Hellman in 1976, although it had been separately invented a few years 201 * earlier within GCHQ, the British signals intelligence agency, by James 202 * H. Ellis, Clifford Cocks and Malcolm J. Williamson but was kept 203 * classified. In 2002, Hellman suggested the algorithm be called 204 * Diffie–Hellman–Merkle key exchange in recognition of Ralph Merkle's 205 * contribution to the invention of public-key cryptography (Hellman, 206 * 2002).</p> 207 * <p>Although Diffie–Hellman key agreement itself is an anonymous 208 * (non-authenticated) key-agreement protocol, it provides the basis for a 209 * variety of authenticated protocols, and is used to provide <i>perfect 210 * forward secrecy</i> in Transport Layer Security's ephemeral modes 211 * (referred to as EDH or DHE depending on the cipher 212 * suite).</p></blockquote> 213 * <p>This method performs the following calculation:</p> 214 * <pre><code>K = remoteSecret<sup>localSecret</sup> mod prime</code></pre> 215 * 216 * @param prime A large prime that is known to both communication 217 * partners. 218 * @param localSecret The secret number that was used to create the 219 * public value sent to the other party. 220 * @param remoteSecret The public value that the other party created. 221 * @return The encryption key <i>K</i>. 222 * 223 * @see <a href="https://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange">Wikipedia: Diffie–Hellman key exchange</a> 224 */ 225 @API( status = STABLE, since = "0.0.5" ) 226 public static final BigInteger calculateDiffieHellmanEncryptionKey( final BigInteger prime, final BigInteger localSecret, final BigInteger remoteSecret ) 227 { 228 final var retValue = requireNonNullArgument( remoteSecret, "remoteSecret" ) 229 .modPow( requireNonNullArgument( localSecret, "localSecret" ), requireNonNullArgument( prime, "prime" ) ); 230 231 //---* Done *---------------------------------------------------------- 232 return retValue; 233 } // calculateDiffieHellmanEncryptionKey() 234 235 /** 236 * <p>{@summary Calculates the value that will be transmitted to the other 237 * party on the exchange of an encryption key using the 238 * Diffie-Hellman-Merkle key exchange scheme.}</p>> 239 * <p>This method performs the following calculation:</p> 240 * <pre><code>A = root<sup>random</sup> mod prime</code></pre>. 241 * 242 * @param prime A large prime that is known to both communication partners. 243 * @param root A primitive root <i>mod {@code prime}</i>. 244 * @param localSecret The secret random number. 245 * @return The value <i>A</i> to transmit to the other party. 246 * 247 * @see #calculateDiffieHellmanEncryptionKey(BigInteger,BigInteger,BigInteger) 248 */ 249 @API( status = STABLE, since = "0.0.5" ) 250 public static final BigInteger calculateDiffieHellmanPublicValue( final BigInteger prime, final BigInteger root, final BigInteger localSecret ) 251 { 252 final var retValue = requireNonNullArgument( root, "root" ) 253 .modPow( requireNonNullArgument( localSecret, "localSecret" ), requireNonNullArgument( prime, "prime" ) ); 254 255 //---* Done *---------------------------------------------------------- 256 return retValue; 257 } // calculateDiffieHellmanPublicValue() 258 259 /** 260 * Calculates a checksum for the given file, based on the MD5 algorithm. 261 * 262 * @param file The file. 263 * @return The check sum. 264 * @throws IOException Something went wrong on reading the file. 265 */ 266 @API( status = STABLE, since = "0.0.5" ) 267 public static final String calculateMD5CheckSum( final File file ) throws IOException 268 { 269 requireNonNullArgument( file, "file" ); 270 271 String retValue = null; 272 synchronized( m_MD5MessageDigest ) 273 { 274 final var hash = determineCheckSum( file, m_MD5MessageDigest ); 275 retValue = HexFormat.of().withUpperCase().formatHex( hash ); 276 } 277 278 //---* Done *---------------------------------------------------------- 279 return retValue; 280 } // calculateMD5CheckSum() 281 282 /** 283 * Calculates a checksum for the given file, based on the SHA-1 algorithm. 284 * 285 * @param file The file. 286 * @return The check sum. 287 * @throws IOException Something went wrong on reading the file. 288 */ 289 @API( status = STABLE, since = "0.0.5" ) 290 public static final String calculateSHACheckSum( final File file ) throws IOException 291 { 292 requireNonNullArgument( file, "file" ); 293 294 String retValue = null; 295 synchronized( m_SHA1MessageDigest ) 296 { 297 final var hash = determineCheckSum( file, m_SHA1MessageDigest ); 298 retValue = HexFormat.of().withUpperCase().formatHex( hash ); 299 } 300 301 //---* Done *---------------------------------------------------------- 302 return retValue; 303 } // calculateSHACheckSum() 304 305 /** 306 * Calculates a checksum for the given file, based on the SHA-256 307 * algorithm. 308 * 309 * @param file The file. 310 * @return The check sum. 311 * @throws IOException Something went wrong on reading the file. 312 */ 313 @API( status = STABLE, since = "0.0.8" ) 314 public static final String calculateSHA256CheckSum( final File file ) throws IOException 315 { 316 requireNonNullArgument( file, "file" ); 317 318 String retValue = null; 319 synchronized( m_SHA256MessageDigest ) 320 { 321 final var hash = determineCheckSum( file, m_SHA256MessageDigest ); 322 retValue = HexFormat.of().withUpperCase().formatHex( hash ); 323 } 324 325 //---* Done *---------------------------------------------------------- 326 return retValue; 327 } // calculateSHA256CheckSum() 328 329 /** 330 * Creates a MD5 hash from the given string.<br> 331 * <br>The output string will contain the digits from {@code 0xA} to 332 * {@code 0xF} all as lower 333 * case.<br> 334 * <br>Use this method to create the values for password fields; it is 335 * not very efficient for calculating the hash value for (large) files as 336 * it would require to load the whole file into memory. 337 * 338 * @param input The source String; may be {@code null}. 339 * @return The String with the hash value or {@code null} if the 340 * input parameter was already {@code null}. 341 * 342 * @see #calculateMD5CheckSum(File) 343 */ 344 @API( status = STABLE, since = "0.0.5" ) 345 public static final String calculateMD5Hash( final CharSequence input ) 346 { 347 String retValue = null; 348 if( nonNull( input ) ) 349 { 350 final var inputBytes = input.toString().getBytes( UTF8 ); 351 retValue = HexFormat.of().withUpperCase().formatHex( calculateMD5Hash( inputBytes ) ).toLowerCase( Locale.ROOT ); 352 } 353 354 //---* Done *---------------------------------------------------------- 355 return retValue; 356 } // calculateMD5Hash() 357 358 /** 359 * Creates a MD5 hash from the given byte sequence.<br> 360 * <br> This method is not very efficient for calculating the hash value 361 * for (large) files as it would require to load the whole file into 362 * memory. 363 * 364 * @param input The byte array to hash. 365 * @return The byte array with the hash. 366 * 367 * @see #calculateMD5CheckSum(File) 368 */ 369 @API( status = STABLE, since = "0.0.5" ) 370 public static final byte [] calculateMD5Hash( final byte [] input ) 371 { 372 requireNonNullArgument( input, "input" ); 373 374 byte [] retValue = null; 375 synchronized( m_MD5MessageDigest ) 376 { 377 m_MD5MessageDigest.reset(); 378 m_MD5MessageDigest.update( input ); 379 retValue = m_MD5MessageDigest.digest(); 380 } 381 382 //---* Done *---------------------------------------------------------- 383 return retValue; 384 } // calculateMD5Hash() 385 386 /** 387 * Creates an SHA-1 hash from the given string.<br> 388 * <br>The output string will contain the digits from {@code 0xA} to 389 * {@code 0xF} all as lower 390 * case.<br> 391 * <br>Use this method to create the values for password fields; it is 392 * not very efficient for calculating the hash value for (large) files as 393 * it would require to load the whole file into memory. 394 * 395 * @param input The source String; may be {@code null}. 396 * @return The String with the hash value or {@code null} if the 397 * input parameter was already {@code null}. 398 * 399 * @see #calculateSHACheckSum(File) 400 */ 401 @API( status = STABLE, since = "0.0.5" ) 402 public static final String calculateSHA1Hash( final CharSequence input ) 403 { 404 String retValue = null; 405 if( nonNull( input ) ) 406 { 407 final var inputBytes = input.toString().getBytes( UTF8 ); 408 retValue = HexFormat.of().withLowerCase().formatHex( calculateSHA1Hash( inputBytes ) ); 409 } 410 411 //---* Done *---------------------------------------------------------- 412 return retValue; 413 } // calculateSHA1Hash() 414 415 /** 416 * Creates an SHA-1 hash from the given byte sequence.<br> 417 * <br> This method is not very efficient for calculating the hash value 418 * for (large) files as it would require to load the whole file into 419 * memory. 420 * 421 * @param input The byte array to hash. 422 * @return The byte array with the hash. 423 * 424 * @see #calculateSHACheckSum(File) 425 */ 426 @API( status = STABLE, since = "0.0.5" ) 427 public static final byte [] calculateSHA1Hash( final byte [] input ) 428 { 429 requireNonNullArgument( input, "input" ); 430 431 byte [] retValue = null; 432 synchronized( m_SHA1MessageDigest ) 433 { 434 m_SHA1MessageDigest.reset(); 435 m_SHA1MessageDigest.update( input ); 436 retValue = m_SHA1MessageDigest.digest(); 437 } 438 439 //---* Done *---------------------------------------------------------- 440 return retValue; 441 } // calculateSHA1Hash() 442 443 /** 444 * Creates an SHA-256 hash from the given string.<br> 445 * <br>The output string will contain the digits from {@code 0xA} to 446 * {@code 0xF} all as lower 447 * case.<br> 448 * <br>Use this method to create the values for password fields; it is 449 * not very efficient for calculating the hash value for (large) files as 450 * it would require to load the whole file into memory. 451 * 452 * @param input The source String; may be {@code null}. 453 * @return The String with the hash value or {@code null} if the 454 * input parameter was already {@code null}. 455 * 456 * @see #calculateSHA256CheckSum(File) 457 */ 458 @API( status = STABLE, since = "0.0.8" ) 459 public static final String calculateSHA256Hash( final CharSequence input ) 460 { 461 String retValue = null; 462 if( nonNull( input ) ) 463 { 464 final var inputBytes = input.toString().getBytes( UTF8 ); 465 retValue = HexFormat.of().withLowerCase().formatHex( calculateSHA256Hash( inputBytes ) ); 466 } 467 468 //---* Done *---------------------------------------------------------- 469 return retValue; 470 } // calculateSHA256Hash() 471 472 /** 473 * Creates an SHA-256 hash from the given byte sequence.<br> 474 * <br> This method is not very efficient for calculating the hash value 475 * for (large) files as it would require to load the whole file into 476 * memory. 477 * 478 * @param input The byte array to hash. 479 * @return The byte array with the hash. 480 * 481 * @see #calculateSHA256CheckSum(File) 482 */ 483 @API( status = STABLE, since = "0.0.8" ) 484 public static final byte [] calculateSHA256Hash( final byte [] input ) 485 { 486 requireNonNullArgument( input, "input" ); 487 488 byte [] retValue = null; 489 synchronized( m_SHA256MessageDigest ) 490 { 491 m_SHA256MessageDigest.reset(); 492 m_SHA256MessageDigest.update( input ); 493 retValue = m_SHA256MessageDigest.digest(); 494 } 495 496 //---* Done *---------------------------------------------------------- 497 return retValue; 498 } // calculateSHA256Hash() 499} 500// class SecurityUtils() 501 502/* 503 * End of File 504 */