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 */