All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.authlete.mdoc.IssuerSignedBuilder Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (C) 2023-2024 Authlete, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.authlete.mdoc;


import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.security.interfaces.ECPrivateKey;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import com.authlete.cbor.CBORByteArray;
import com.authlete.cbor.CBORDiagnosticNotationParser;
import com.authlete.cbor.CBORString;
import com.authlete.cbor.CBORizer;
import com.authlete.cose.COSEEC2Key;
import com.authlete.cose.COSEException;
import com.authlete.cose.COSEKey;
import com.authlete.cose.COSEProtectedHeader;
import com.authlete.cose.COSEProtectedHeaderBuilder;
import com.authlete.cose.COSESign1;
import com.authlete.cose.COSESign1Builder;
import com.authlete.cose.COSESigner;
import com.authlete.cose.COSEUnprotectedHeader;
import com.authlete.cose.COSEUnprotectedHeaderBuilder;
import com.authlete.cose.SigStructure;
import com.authlete.cose.SigStructureBuilder;
import com.authlete.cose.constants.COSEAlgorithms;
import com.authlete.cose.constants.COSEEllipticCurves;


/**
 * Utility to build an {@link IssuerSigned} instance.
 *
 * 

* An {@code IssuerSigned} instanced is used to create a {@link Document} * instance. *

* * @since 1.5 * * @see ISO/IEC 18013-5:2021 */ public class IssuerSignedBuilder { private static final SecureRandom RANDOM = new SecureRandom(); private String mDocType; private Map mClaims; private ValidityInfo mValidityInfo; private COSEKey mDeviceKey; private COSEEC2Key mIssuerKey; private List mIssuerCertChain; private CBORizer mCBORizer; /** * Get the DocType. * *

* The value is used as the value of the {@code "docType"} parameter of * the {@link MobileSecurityObject}. *

* * @return * The DocType. */ public String getDocType() { return mDocType; } /** * Set the DocType. * *

* The value is used as the value of the {@code "docType"} parameter of * the {@link MobileSecurityObject} structure. *

* * @param docType * The DocType. * * @return * return {@code this} object. */ public IssuerSignedBuilder setDocType(String docType) { mDocType = docType; return this; } /** * Get the claims used to create {@link IssuerSignedItem}s. * *

* See the description of the {@link #setClaims(Map)} method for details * about the format. *

* * @return * The claims used to create {@link IssuerSignedItem}s. */ public Map getClaims() { return mClaims; } /** * Set the claims used to create {@link IssuerSignedItem}s. * *

* The keys of the top-level properties in the {@code claims} map must be * strings representing name spaces, and their values must be JSON objects, * each of which contains claims under the corresponding name space. *

* *

* The following JSON shows the structure that the {@code claims} map * should have. *

* *
     * {
     *     "com.example.namespace1" : {
     *         "claimName1": "claimValue1",
     *         "claimName2": true,
     *         "claimName3": 1
     *     },
     *     "com.example.namespace2" : {
     *         "claimName4": [ "element1", "element2" ],
     *         "claimName5": {
     *           "subClaimName1": "subClaimValue1"
     *         }
     *     }
     * }
     * 
* *

* Types of claim values can be strings, boolean values, integers, * floating-point numbers, arrays ({@code List}) and maps ({@code Map}), * which are natural representations of JSON values. *

* *

* However, there may be cases where CBOR-specific data need to be embedded. * For example, the {@code "birth_date"} claim may require the "full-date" * tag (defined in RFC 8943) and the {@code "portrait"} claim may require binary data. *

* *

* To embed CBOR-specific data, a {@link CBORizer} instance with an * implementation of the {@link CBORDiagnosticNotationParser} interface * needs to be set by calling the {@link #setCBORizer(CBORizer)} method. * Such {@code CBORizer} will interpret strings written in the CBOR * Diagnostic Notation (RFC 8949 Section 8, * RFC 8610 * Appendix G) with a special prefix (e.g. {@code "cbor:"}), and * convert them into CBOR-specific data. As a result, input data like * below will be accepted and CBOR-specific data will be embedded * accordingly. *

* *
     * {
     *     "com.example.namespace3": {
     *         "birth_date": "cbor:1004(\"1974-05-06\")",
     *         "portrait": "cbor:h'0102.....'"
     *     }
     * }
     * 
* * @param claims * The claims used to create {@link IssuerSignedItem}s. * * @return * {@code this} object. */ public IssuerSignedBuilder setClaims(Map claims) { mClaims = claims; return this; } /** * Get the validity information of the Mobile Security Object. * *

* The value is used as the value of the {@code "validityInfo"} parameter of * the {@link MobileSecurityObject} structure. *

* * @return * The validity information of the Mobile Security Object. */ public ValidityInfo getValidityInfo() { return mValidityInfo; } /** * Set the validity information of the Mobile Security Object. * *

* The value is used as the value of the {@code "validityInfo"} parameter of * the {@link MobileSecurityObject} structure. *

* * @param info * The validity information of the Mobile Security Object. * * @return * {@code this} object. */ public IssuerSignedBuilder setValidityInfo(ValidityInfo info) { mValidityInfo = info; return this; } /** * Get the device key. * *

* The value is used as the value of the {@code "deviceKey"} parameter of * the {@link DeviceKeyInfo} structure. *

* * @return * The device key. */ public COSEKey getDeviceKey() { return mDeviceKey; } /** * Set the device key. * *

* The value is used as the value of the {@code "deviceKey"} parameter of * the {@link DeviceKeyInfo} structure. *

* * @param deviceKey * The device key. * * @return * {@code this} object. */ public IssuerSignedBuilder setDeviceKey(COSEKey deviceKey) { mDeviceKey = deviceKey; return this; } /** * Get the issuer key used to sign the {@code IssuerAuth} structure * ({@link COSESign1}). * * @return * The issuer key. */ public COSEEC2Key getIssuerKey() { return mIssuerKey; } /** * Set the issuer key used to sign the {@code IssuerAuth} structure * ({@link COSESign1}). * * @param issuerKey * The issuer key. * * @return * {@code this} object. */ public IssuerSignedBuilder setIssuerKey(COSEEC2Key issuerKey) { mIssuerKey = issuerKey; return this; } /** * Get the certificate chain for the issuer key. * *

* The value is embedded in the unprotected header of the {@code IssuerAuth} * structure ({@link COSESign1}) as the value of the {@code x5chain} header * parameter. *

* * @return * The certificate chain for the issuer key. * * @see RFC 9360 CBOR Object Signing and Encryption (COSE): * Header Parameters for Carrying and Referencing X.509 Certificates */ public List getIssuerCertChain() { return mIssuerCertChain; } /** * Set the certificate chain for the issuer key. * *

* The value is embedded in the unprotected header of the {@code IssuerAuth} * structure ({@link COSESign1}) as the value of the {@code x5chain} header * parameter. *

* * @param chain * The certificate chain for the issuer key. * * @return * {@code this} object. * * @see RFC 9360 CBOR Object Signing and Encryption (COSE): * Header Parameters for Carrying and Referencing X.509 Certificates */ public IssuerSignedBuilder setIssuerCertChain(List chain) { mIssuerCertChain = chain; return this; } /** * Get the {@link CBORizer} to convert given claim values into CBOR items. * *

* If there are claim values expressed in the CBOR Diagnostic Notation like * {@code "cbor:h'0102'"} and {@code "cbor:0(\"2013-03-21T20:04:00Z\")"}, a * {@code CBORizer} instance with {@link CBORDiagnosticNotationParser} needs * to be explicitly set. Otherwise, strings expressed in the CBOR Diagnostic * Notation are embedded as CBOR text strings without any conversion. *

* * @return * The {@code CBORizer}. */ public CBORizer getCBORizer() { return mCBORizer; } /** * Set the {@link CBORizer} to convert given claim values into CBOR items. * *

* If there are claim values expressed in the CBOR Diagnostic Notation like * {@code "cbor:h'0102'"} and {@code "cbor:0(\"2013-03-21T20:04:00Z\")"}, a * {@code CBORizer} instance with {@link CBORDiagnosticNotationParser} needs * to be explicitly set. Otherwise, strings expressed in the CBOR Diagnostic * Notation are embedded as CBOR text strings without any conversion. *

* * @param cborizer * The {@code CBORizer}. * * @return * {@code this} object. */ public IssuerSignedBuilder setCBORizer(CBORizer cborizer) { mCBORizer = cborizer; return this; } /** * Build an {@link IssuerSigned} instance. * * @return * A new {@link IssuerSigned} instance. * * @throws COSEException * Signing with the issuer key failed. * * @throws CertificateEncodingException * The DER representation of an X.509 certificate in the issuer's * certificate chain failed to be obtained. */ public IssuerSigned build() throws COSEException, CertificateEncodingException { // Check some input parameters. checkInput(); // nameSpaces IssuerNameSpaces issuerNameSpaces = buildIssuerNameSpaces(); // issuerAuth COSESign1 issuerAuth = buildIssuerAuth(issuerNameSpaces); // IssuerSigned = { // ? "nameSpaces" : IssuerNameSpaces // "issuerAuth" : IssuerAuth // } return new IssuerSigned(issuerNameSpaces, issuerAuth); } private void checkInput() { if (getDocType() == null) { throw new IllegalStateException("Doc type is not set."); } if (getIssuerKey() == null) { throw new IllegalStateException("Issuer key is not set."); } if (getIssuerCertChain() == null) { throw new IllegalStateException("Issuer certificate chain is not set."); } if (getIssuerCertChain().size() == 0) { throw new IllegalStateException("Issuer certificate chain is empty."); } } private IssuerNameSpaces buildIssuerNameSpaces() { // The input claims. The expected structure is as follows. // // { // "namespace1": { // "claimName1": "claimValue1", // ... // }, // "namespace2": { // "claimName2": "claimValue2", // ... // } // } // Map claims = prepareClaims(); // The cborizer that converts claim values into CBOR items. CBORizer cborizer = prepareCBORizer(); // Digest ID generator for IssuerSignedItem's. In this implementation, // digest IDs are unique across all name spaces. SequentialIntegerGenerator digestIdGenerator = new SequentialIntegerGenerator(); // Entries in the IssuerNameSpaces structure. List issuerNameSpacesEntries = new ArrayList<>(); // For each pair of a name space and sub claims under the name space. for (Map.Entry entry : claims.entrySet()) { // The name space. String nameSpace = entry.getKey(); // The sub claims under the name space. Object subClaims = entry.getValue(); // Create a list of IssuerSignedItemBytes from the sub claims. List itemBytesList = buildItemBytesList(nameSpace, subClaims, digestIdGenerator, cborizer); // NameSpace => [ + IssuerSignedItemBytes ] IssuerNameSpacesEntry issuerNameSpacesEntry = new IssuerNameSpacesEntry(nameSpace, itemBytesList); // Add to the list. issuerNameSpacesEntries.add(issuerNameSpacesEntry); } // IssuerNameSpaces = { // + NameSpace => [ + IssuerSignedItemBytes ] // } return new IssuerNameSpaces(issuerNameSpacesEntries); } @SuppressWarnings("unchecked") private List buildItemBytesList( String nameSpace, Object subClaims, SequentialIntegerGenerator digestIdGenerator, CBORizer cborizer) { // If the value corresponding to the name space is not a JSON object. if (!(subClaims instanceof Map)) { throw new IllegalArgumentException(String.format( "The value for the name space '%s' is not a JSON object.", nameSpace)); } List itemBytesList = new ArrayList<>(); // For each sub claim under the name space. for (Map.Entry subClaim : ((Map)subClaims).entrySet()) { // Create an IssuerSignedItem from the sub claim. IssuerSignedItem item = buildIssuerSignedItem(subClaim, digestIdGenerator, cborizer); // IssuerSignedItemBytes = #6.24(bstr .cbor IssuerSignedItem) IssuerSignedItemBytes itemBytes = new IssuerSignedItemBytes(item); // Add to the list. itemBytesList.add(itemBytes); } return itemBytesList; } private IssuerSignedItem buildIssuerSignedItem( Map.Entry subClaim, SequentialIntegerGenerator digestIdGenerator, CBORizer cborizer) { // Digest ID int digestID = digestIdGenerator.next(); // Random bytes byte[] random = generateRandom(); // Element identifier String elementIdentifier = subClaim.getKey(); // Element value Object elementValue = cborizer.cborize(subClaim.getValue()); // IssuerSignedItem = { // "digestID" : uint, // "random" : bstr, // "elementIdentifier" : DataElementIdentifier, // "elementValue" : DataElementValue // } return new IssuerSignedItem(digestID, random, elementIdentifier, elementValue); } private COSESign1 buildIssuerAuth(IssuerNameSpaces issuerNameSpaces) throws COSEException, CertificateEncodingException { // The algorithm for signing. int alg = determineIssuerAuthSigningAlgorithm(); // Protected header COSEProtectedHeader protectedHeader = prepareIssuerAuthProtectedHeader(alg); // Unprotected header COSEUnprotectedHeader unprotectedHeader = prepareIssuerAuthUnprotectedHeader(); // Payload CBORByteArray payload = prepareIssuerAuthPayload(issuerNameSpaces); // Sig_structure, which is the target to sign. SigStructure sigStructure = prepareSigStructure(protectedHeader, payload); // Sign the Sig_structure. byte[] signature = sign(sigStructure, alg); // IssuerAuth = COSE_Sign1 ; The payload is MobileSecurityObjectBytes return new COSESign1Builder() .protectedHeader(protectedHeader) .unprotectedHeader(unprotectedHeader) .payload(payload) .signature(signature) .build(); } private int determineIssuerAuthSigningAlgorithm() { // The value of the 'alg' parameter of the issuer key. Object alg = getIssuerKey().getAlg(); // If the issuer key contains the 'alg' parameter. if (alg != null) { return (alg instanceof String) ? COSEAlgorithms.getValueByName((String)alg) : ((Number)alg).intValue(); } // The value of the 'crv' parameter of the issuer key. Object crv = getIssuerKey().getCrv(); // If the issuer key does not contain the 'crv' parameter. if (crv == null) { throw new IllegalArgumentException( "The issuer key does not contain the 'crv' parameter."); } // ISO/IEC 18013-5:2021, 9.1.2.4 Signing method and structure for MSO // // The issuing authority infrastructure shall use one of the following // signature algorithms for calculating the signature over the MSO: // “ES256” (ECDSA with SHA-256), “ES384” (ECDSA with SHA-384), “ES512” // (ECDSA with SHA-512) or “EdDSA” (EdDSA). ”ES256” shall be used with // curves P-256 and brainpoolP256r1. “ES384” shall be used with curves // P-384, brainpoolP320r1 and brainpoolP384r1. “ES512” shall be used // with curves P-521 and brainpoolP512r1. “EdDSA” shall be used with // curves Ed25519 and Ed448. For verifying the signature, the mdoc // reader shall support all of these signature algorithms and curves. // // If the curve is P-256. if (crv.equals(COSEEllipticCurves.P_256) || crv.equals(COSEEllipticCurves.P_256_NAME)) { // ES256 return COSEAlgorithms.ES256; } // If the curve is P-384. else if (crv.equals(COSEEllipticCurves.P_384) || crv.equals(COSEEllipticCurves.P_384_NAME)) { // ES384 return COSEAlgorithms.ES384; } // If the curve is P-521. else if (crv.equals(COSEEllipticCurves.P_521) || crv.equals(COSEEllipticCurves.P_521_NAME)) { // ES512 return COSEAlgorithms.ES512; } // If the curve is Ed25519. else if (crv.equals(COSEEllipticCurves.Ed25519) || crv.equals(COSEEllipticCurves.Ed25519_NAME)) { // EdDSA return COSEAlgorithms.EdDSA; } // If the curve is Ed448. else if (crv.equals(COSEEllipticCurves.Ed448) || crv.equals(COSEEllipticCurves.Ed448_NAME)) { // EdDSA return COSEAlgorithms.EdDSA; } throw new IllegalArgumentException("The curve of the issuer key is not supported."); } private COSEProtectedHeader prepareIssuerAuthProtectedHeader(int alg) { // ISO/IEC 18013-5:2021, 9.1.2.4 Signing method and structure for MSO // // The alg element (RFC 8152) shall be included as an element in the // protected header. Other elements should not be present in the // protected header. // // Build a protected header with the 'alg' parameter. return new COSEProtectedHeaderBuilder().alg(alg).build(); } private COSEUnprotectedHeader prepareIssuerAuthUnprotectedHeader() throws CertificateEncodingException { // ISO/IEC 18013-5:2021, 9.1.2.4 Signing method and structure for MSO // // The certificate containing the public key belonging to the private // key used to sign the MSO shall be included as an x5chain element // as described in RFC: CBOR Object Signing and Encryption (COSE): // Headers for carrying and referencing X.509 certificates. It shall // be included as an unprotected header element. The x5chain element // shall include at least one certificate and may contain more. // // Build an unprotected header with the 'x5chain' parameter. return new COSEUnprotectedHeaderBuilder() .x5chain(getIssuerCertChain()).build(); } private CBORByteArray prepareIssuerAuthPayload(IssuerNameSpaces issuerNameSpaces) { // MobileSecurityObjectBytes = #6.24(bstr .cbor MobileSecurityObject) MobileSecurityObjectBytes msoBytes = buildMobileSecurityObjectBytes(issuerNameSpaces); // The definitions of IssuerAuth and MobileSecurityObjectBytes in the // ISO/IEC 18013-5 are as follows: // // IssuerAuth = COSE_Sign1; The payload is MobileSecurityObjectBytes // // MobileSecurityObjectBytes = #6.24(bstr .cbor MobileSecurityObject) // // These give the impression that MobileSecurityObjectBytes (which starts // with a CBOR tag) is directly used as the payload of COSE_Sign1. // // However, it is necessary to further convert MobileSecurityObjectBytes // into a byte string. // CBORByteArray payload = new CBORByteArray(msoBytes.encode(), msoBytes); // Add a comment just for CBORItem.prettify(). payload.setComment("payload"); return payload; } private SigStructure prepareSigStructure( COSEProtectedHeader protectedHeader, CBORByteArray payload) { return new SigStructureBuilder() .signature1() .bodyAttributes(protectedHeader) .payload(payload) .build(); } private byte[] sign(SigStructure sigStructure, int alg) throws COSEException { // The private key for signing. ECPrivateKey privateKey = getIssuerKey().toECPrivateKey(); // Create a signer with the private key. COSESigner signer = new COSESigner(privateKey); // Sign the Sig_structure (= generate a signature). return signer.sign(sigStructure, alg); } private MobileSecurityObjectBytes buildMobileSecurityObjectBytes(IssuerNameSpaces issuerNameSpaces) { // Build a Mobile Security Object (MSO). MobileSecurityObject mso = buildMobileSecurityObject(issuerNameSpaces); // MobileSecurityObjectBytes = #6.24(bstr .cbor MobileSecurityObject) return new MobileSecurityObjectBytes(mso); } private MobileSecurityObject buildMobileSecurityObject(IssuerNameSpaces issuerNameSpaces) { // Value digests ValueDigests valueDigests = buildValueDigests(issuerNameSpaces); // Device key info DeviceKeyInfo deviceKeyInfo = buildDeviceKeyInfo(issuerNameSpaces); // MobileSecurityObject = { // "version" : tstr, // "digestAlgorithm" : tstr, // "valueDigests" : ValueDigests, // "deviceKeyInfo" : DeviceKeyInfo, // "docType" : tstr, // "validityInfo" : ValidityInfo // } return new MobileSecurityObject( valueDigests, deviceKeyInfo, getDocType(), getValidityInfo()); } @SuppressWarnings("unchecked") private ValueDigests buildValueDigests(IssuerNameSpaces issuerNameSpaces) { List issuerNameSpacesEntries = (List)issuerNameSpaces.getPairs(); List valueDigestsEntries = new ArrayList<>(); // For each "NameSpace => [ + IssuerSignedItemBytes ]" for (IssuerNameSpacesEntry issuerNameSpacesEntry : issuerNameSpacesEntries) { // The name space. CBORString nameSpace = issuerNameSpacesEntry.getNameSpace(); // The list of IssuerSignedItemBytes. List issuerSignedItemBytesList = issuerNameSpacesEntry.getIssuerSignedItemBytesList(); // Build a DigestIDs from the list of IssuerSignedItemBytes. DigestIDs digestIDs = buildDigestIDs(issuerSignedItemBytesList); // NameSpace => DigestIDs ValueDigestsEntry valueDigestsEntry = new ValueDigestsEntry(nameSpace, digestIDs); // Add to the list. valueDigestsEntries.add(valueDigestsEntry); } // ValueDigests = { // + NameSpace => DigestIDs // } return new ValueDigests(valueDigestsEntries); } private DigestIDs buildDigestIDs( List issuerSignedItemBytesList) { List digestIDsEntries = new ArrayList<>(); for (IssuerSignedItemBytes issuerSignedItemBytes : issuerSignedItemBytesList) { // IssuerSignedItem IssuerSignedItem issuerSignedItem = issuerSignedItemBytes.getIssuerSignedItem(); // Digest ID int digestID = issuerSignedItem.getDigestID(); // Compute the digest. byte[] digest = computeDigest(issuerSignedItemBytes.encode()); // DigestID => Digest DigestIDsEntry digestIDsEntry = new DigestIDsEntry(digestID, digest); // Add to the list. digestIDsEntries.add(digestIDsEntry); } // DigestIDs = { // + DigestID => Digest // } return new DigestIDs(digestIDsEntries); } private DeviceKeyInfo buildDeviceKeyInfo(IssuerNameSpaces issuerNameSpaces) { // Device key. COSEKey deviceKey = getDeviceKey(); if (deviceKey == null) { return null; } // Key authorizations KeyAuthorizations keyAuthorizations = buildKeyAuthorizations(issuerNameSpaces); // DeviceKeyInfo = { // "deviceKey" : DeviceKey, // ? "keyAuthorizations" : KeyAuthorizations, // ? "keyInfo" : KeyInfo // } return new DeviceKeyInfo(deviceKey, keyAuthorizations, null); } private KeyAuthorizations buildKeyAuthorizations(IssuerNameSpaces issuerNameSpaces) { // Authorized name spaces AuthorizedNameSpaces authorizedNameSpaces = buildAuthorizedNameSpaces(issuerNameSpaces); // KeyAuthorizations = { // ? "nameSpaces" : AuthorizedNameSpaces, // ? "dataElements" : AuthorizedDataElements // } return new KeyAuthorizations(authorizedNameSpaces, null); } @SuppressWarnings("unchecked") private AuthorizedNameSpaces buildAuthorizedNameSpaces(IssuerNameSpaces issuerNameSpaces) { List issuerNameSpaceEntries = (List)issuerNameSpaces.getPairs(); List nameSpaces = issuerNameSpaceEntries.stream() .map(entry -> entry.getNameSpace()) .collect(Collectors.toList()); // AuthorizedNameSpaces = [ + NameSpace ] return new AuthorizedNameSpaces(nameSpaces); } private Map prepareClaims() { // The claims given to this builder as input data. Map claims = getClaims(); if (claims == null) { claims = Collections.emptyMap(); } return claims; } private CBORizer prepareCBORizer() { // The cborizer given to this builder. CBORizer cborizer = getCBORizer(); if (cborizer == null) { cborizer = new CBORizer(); } return cborizer; } private static byte[] generateRandom() { // ISO/IEC 18013-5:2021, 9.1.2.5 Message digest function // // Each IssuerSignedItem shall also contain an unpredictable random // or pseudorandom value. This value shall be different for each // IssuerSignedItem and shall have a minimum length of 16 bytes. // return generateRandomBytes(16); } /** * Generate a byte array of the specified size containing a random value. */ private static byte[] generateRandomBytes(int size) { byte[] bytes = new byte[size]; RANDOM.nextBytes(bytes); return bytes; } private static byte[] computeDigest(byte[] input) { return computeDigest(input, "SHA-256"); } /** * Compute the digest of the input with the specified hash algorithm. * * @throws IllegalArgumentException * The specified hash algorithm is not supported. */ private static byte[] computeDigest(byte[] input, String hashAlgorithm) { try { return MessageDigest.getInstance(hashAlgorithm).digest(input); } catch (NoSuchAlgorithmException cause) { // Error message String message = String.format( "The hash algorithm '%s' is not supported.", hashAlgorithm); throw new RuntimeException(message, cause); } } private static class SequentialIntegerGenerator { private int number; public int next() { return ++number; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy