package de.persosim.simulator.crypto; import java.math.BigInteger; import java.security.GeneralSecurityException; import java.security.InvalidAlgorithmParameterException; import java.security.KeyFactory; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.PrivateKey; import java.security.PublicKey; import java.security.SecureRandom; import java.security.Signature; import java.security.interfaces.ECPublicKey; import java.security.spec.ECField; import java.security.spec.ECFieldFp; import java.security.spec.ECParameterSpec; import java.security.spec.ECPoint; import java.security.spec.ECPublicKeySpec; import java.security.spec.EllipticCurve; import java.security.spec.InvalidKeySpecException; import java.security.spec.KeySpec; import java.util.Arrays; import org.globaltester.cryptoprovider.Crypto; import de.persosim.simulator.tlv.Asn1; import de.persosim.simulator.tlv.ConstructedTlvDataObject; import de.persosim.simulator.tlv.PrimitiveTlvDataObject; import de.persosim.simulator.tlv.TlvConstants; import de.persosim.simulator.tlv.TlvDataObject; import de.persosim.simulator.tlv.TlvDataObjectContainer; import de.persosim.simulator.tlv.TlvTag; import de.persosim.simulator.utils.Utils; /** * This class provides static methods offering support for basic operations in the field of cryptography. * * XXX functional overlap with {@link Crypto} - merge? * * @author slutters * */ public class CryptoUtil { public static final BigInteger ZERO = BigInteger.ZERO; public static final BigInteger ONE = BigInteger.ONE; public static final BigInteger TWO = ONE.add(ONE); public static final BigInteger THREE = TWO.add(ONE); public static final String CIPHER_DELIMITER = "/"; public static final byte[] BITMASK = new byte[]{(byte) 0x01, (byte) 0x02, (byte) 0x04, (byte) 0x08, (byte) 0x10, (byte) 0x20, (byte) 0x40, (byte) 0x80}; public static final byte[] BITMASK_COMPLEMENT = new byte[]{(byte) 0xFE, (byte) 0xFD, (byte) 0xFB, (byte) 0xF7, (byte) 0xEF, (byte) 0xDF, (byte) 0xBF, (byte) 0x7F}; public static final byte ENCODING_UNCOMPRESSED = -1; public static final byte ENCODING_COMPRESSED = 0; public static final byte ENCODING_HYBRID = 1; /** * This method extracts the basic cipher name from the full cipher * String, e.g. it will turn "AES/CBC/NoPadding" into simply "AES". * * @param cAlgNMP * the full cipher name String * @return the basic cipher name */ public static String getCipherNameAsString(String cAlgNMP) { int index = cAlgNMP.indexOf(CIPHER_DELIMITER); if(index < 0) { //if no delimiter is found return cAlgNMP; } else{ return cAlgNMP.substring(0, index); } } /** * This method extracts the cipher mode from the full cipher * String, e.g. it will turn "AES/CBC/NoPadding" into simply "CBC". * * @param cAlgNMP * the full cipher name String * @return the cipher mode */ public static String getCipherAlgorithmModeAsString(String cAlgNMP) { int index; index = cAlgNMP.indexOf(CIPHER_DELIMITER); return cAlgNMP.substring(index + 1, cAlgNMP.indexOf(CIPHER_DELIMITER, index + 1)); } /** * This method extracts the cipher padding from the full cipher * String, e.g. it will turn "AES/CBC/NoPadding" into simply "NoPadding". * * @param cAlgNMP * the full cipher name String * @return the cipher padding */ public static String getCipherAlgorithmPaddingAsString(String cAlgNMP) { int index; index = cAlgNMP.indexOf(CIPHER_DELIMITER); return cAlgNMP.substring(index + 1, cAlgNMP.indexOf(CIPHER_DELIMITER, index + 1)); } /*--------------------------------------------------------------------------------*/ /** * This method returns the X-coordinate of the point returned e.g. by point addition or point doubling. * @param p the prime used by the curve * @param lambda the lambda value specific to the calling method * @param xp the X-coordinate of input point P * @param xq the X-coordinate of input point Q * @return the X-coordinate of the point returned e.g. by point addition or point doubling */ private static BigInteger computeXr(BigInteger p, BigInteger lambda, BigInteger xp, BigInteger xq) { return (((lambda.modPow(TWO, p).subtract(xq))).subtract(xp)).mod(p); } /** * This method returns the X-coordinate of the point returned e.g. by point addition or point doubling. * @param p the prime used by the curve * @param lambda the lambda value specific to the calling method * @param xp the X-coordinate of input point P * @return the X-coordinate of the point returned e.g. by point addition or point doubling */ private static BigInteger computeXr(BigInteger p, BigInteger lambda, BigInteger xp) { return computeXr(p, lambda, xp, xp); } /** * This method returns the Y-coordinate of the point R returned e.g. by point addition or point doubling. * @param p the prime used by the curve * @param lambda lambda the lambda value specific to the calling method * @param xp the X-coordinate of input point P * @param yp the Y-coordinate of input point P * @param xr the X-coordinate of the result point R * @return */ private static BigInteger computeYr(BigInteger p, BigInteger lambda, BigInteger xp, BigInteger yp, BigInteger xr) { return ((lambda.multiply(xp.subtract(xr)).mod(p)).subtract(yp)).mod(p); } /** * This method performs EC point addition * @param curve the elliptic curve to be used * @param ecPointQ the first point for addition * @param ecPointP the second point for addition * @return the result of the point addition */ public static ECPoint addPoint(EllipticCurve curve, ECPoint ecPointQ, ECPoint ecPointP) { if (ecPointQ.equals(ecPointP)) {return doublePoint(curve, ecPointQ);} if (ecPointQ.equals(ECPoint.POINT_INFINITY)) {return ecPointP;} if (ecPointP.equals(ECPoint.POINT_INFINITY)) {return ecPointQ;} BigInteger p = ((ECFieldFp) curve.getField()).getP(); BigInteger xq = ecPointQ.getAffineX(); BigInteger yq = ecPointQ.getAffineY(); BigInteger xp = ecPointP.getAffineX(); BigInteger yp = ecPointP.getAffineY(); BigInteger lambda = ((yq.subtract(yp)).multiply(xq.subtract(xp).modInverse(p))).mod(p); BigInteger xr = computeXr(p, lambda, xp, xq); BigInteger yr = computeYr(p, lambda, xp, yp, xr); ECPoint ecPointR = new ECPoint(xr, yr); return ecPointR; } /** * This method performs EC point doubling * @param curve the elliptic curve to be used * @param ecPointP the second point for addition * @return the result of the point doubling */ public static ECPoint doublePoint(EllipticCurve curve, ECPoint ecPointP) { if (ecPointP.equals(ECPoint.POINT_INFINITY)) {return ecPointP;} BigInteger p = ((ECFieldFp) curve.getField()).getP(); BigInteger a = curve.getA(); BigInteger xp = ecPointP.getAffineX(); BigInteger yp = ecPointP.getAffineY(); BigInteger lambda = ((((xp.pow(2)).multiply(THREE)).add(a)).multiply((yp.multiply(TWO)).modInverse(p))).mod(p); BigInteger xr = computeXr(p, lambda, xp); BigInteger yr = computeYr(p, lambda, xp, yp, xr); ECPoint ecPointR = new ECPoint(xr, yr); return ecPointR; } /** * This method performs EC scalar point multiplication using Double-and-add method. * The method is optimized for performance performing actual multiplication with scalar.mod(order). * @param curve the elliptic curve to be used * @param order the order of the curve * @param ecPointP the point to be multiplied * @param scalar the scalar multiplier * @return the multiplied EC point */ public static ECPoint scalarPointMultiplication(EllipticCurve curve, BigInteger order, ECPoint ecPointP, BigInteger scalar) { return scalarPointMultiplication(curve, ecPointP, scalar.mod(order)); } /** * This method performs EC scalar point multiplication using Double-and-add * method. For improved performance preferably use * {@link #scalarPointMultiplication(EllipticCurve, BigInteger, ECPoint, BigInteger)} * or make sure the scalar you provide already is taken modulo the order of the * field (scalar.mod(order)). * * @param curve * the elliptic curve to be used * @param ecPointP * the point to be multiplied * @param scalar * the scalar multiplier * @return the multiplied EC point */ public static ECPoint scalarPointMultiplication(EllipticCurve curve, ECPoint ecPointP, BigInteger scalar) { if (ecPointP.equals(ECPoint.POINT_INFINITY)) {return ecPointP;} ECPoint ecPointR = ECPoint.POINT_INFINITY; for (int i = (scalar.bitLength()) - 1; i >= 0; i--) { ecPointR = doublePoint(curve, ecPointR); if (scalar.testBit(i)) { ecPointR = addPoint(curve, ecPointR, ecPointP); } } return ecPointR; } /** * This method encodes an {@link ECPoint} (using uncompressed, compressed or * hybrid encoding according to X9.62). * <p/> * According to ANSI X9.62 EC public point encoding in uncompressed mode is * supposed to look as follows: * uncompressed: 04||x-coordinate||y-coordinate * compressed : 02|03||x-coordinate * hybrid : 06|07||x-coordinate||y-coordinate * If an encoded coordinate does not match the reference length l, it needs * to be padded with leading 00 bytes. * * @param ecPoint * point to be encoded * @param referenceLength * expected length l of each coordinate in bytes * @param encoding * either {@link #ENCODING_UNCOMPRESSED}, {@link #ENCODING_COMPRESSED} or * {@link #ENCODING_HYBRID} * @return byte[] containing the point encoding */ public static byte[] encode(ECPoint ecPoint, int referenceLength, byte encoding) { byte encodingIndicator; byte[] pointEncoding; byte[] xBytes = getProjectedRepresentation(ecPoint, referenceLength, true); byte[] yBytes = getProjectedRepresentation(ecPoint, referenceLength, false); boolean yBitSet = ecPoint.getAffineY().testBit(0); if(encoding == ENCODING_COMPRESSED) { if(yBitSet) { encodingIndicator = (byte) 0x03; } else { encodingIndicator = (byte) 0x02; } pointEncoding = xBytes; } else { if(encoding == ENCODING_UNCOMPRESSED) { encodingIndicator = (byte) 0x04; } else { if(encoding == ENCODING_HYBRID) { if(yBitSet) { encodingIndicator = (byte) 0x07; } else { encodingIndicator = (byte) 0x06; } } else { throw new IllegalArgumentException("unsupported encoding"); } } pointEncoding = Utils.concatByteArrays(xBytes, yBytes); } return Utils.concatByteArrays(new byte[] {encodingIndicator}, pointEncoding); } /** * This method returns a projection of the provided point's selected coordinate. * If the length of the selected encoded coordinate is less than the provided reference length, it is padded to this length. * @param ecPoint the point to work on * @param referenceLength the desired reference length * @param encodeX true: encode x-coordinate, false: encode y-coordinate * @return the projection of the provided point's selected coordinate */ public static byte[] getProjectedRepresentation(ECPoint ecPoint, int referenceLength, boolean encodeX) { BigInteger coordinate; String coordinateName; if(encodeX) { coordinate = ecPoint.getAffineX(); coordinateName = "x"; } else{ coordinate = ecPoint.getAffineY(); coordinateName = "y"; } // extract coordinate byte[] bytes = Utils.toUnsignedByteArray(coordinate); //check coordinate length if (bytes.length > referenceLength) { throw new IllegalArgumentException(coordinateName + "-coordinate of point is larger than reference length"); } // add padding to coordinate if needed if (bytes.length < referenceLength) { byte[] padding = new byte[referenceLength - bytes.length]; Arrays.fill(padding, (byte) 0x00); bytes = Utils.concatByteArrays(padding, bytes); } return bytes; } public static KeyPair generateKeyPair(DomainParameterSet domParamSet, SecureRandom secRandom) throws NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException { KeyPairGenerator keyPairGenerator; keyPairGenerator = KeyPairGenerator.getInstance(domParamSet.getKeyAgreementAlgorithm(), Crypto.getCryptoProvider()); keyPairGenerator.initialize(domParamSet.getKeySpec(), secRandom); return keyPairGenerator.generateKeyPair(); } /** * This method returns a copy of the provided key pair which is updated to the new provided domain parameters. * * @param keyPair the key pair template to be used for creating the key pairs updated to the new domain parameters * @return a copy of the provided key pair which is updated to the new provided domain parameters * @throws NoSuchAlgorithmException * @throws InvalidKeySpecException */ public static KeyPair updateKeyPairToNewDomainParameters(KeyPair keyPair, DomainParameterSet domainParametersMapped) throws NoSuchAlgorithmException, InvalidKeySpecException { // create key specs for ephemeral public key pair according to mapped domain parameters KeySpec[] keySpecsPiccMapped = domainParametersMapped.updateKeySpec(keyPair); KeySpec mappedPrivateKeySpec = keySpecsPiccMapped[0]; KeySpec mappedPublicKeySpec = keySpecsPiccMapped[1]; // Actually create keys from key specs KeyFactory keyFactory = KeyFactory.getInstance(domainParametersMapped.getKeyAgreementAlgorithm(), Crypto.getCryptoProvider()); PrivateKey mappedPrivateKey = keyFactory.generatePrivate(mappedPrivateKeySpec); PublicKey mappedPublicKey = keyFactory.generatePublic(mappedPublicKeySpec); return new KeyPair(mappedPublicKey, mappedPrivateKey); } /** * This method pads the byte representation of a {@link BigInteger} with a * leading single 0x00 byte if it is negative. * * @param value * @return */ private static byte[] getPadded(BigInteger value) { if (value.signum() < 0) { return Utils.concatByteArrays(new byte[1], value.toByteArray()); } return value.toByteArray(); } /** * When an ECDSA signature is given, this method restores the ASN.1 structure * as used by the {@link Signature#verify(byte[])} method * before the actual verification is possible. * * @param signatureData * @return ASN.1 formatted TLV object */ public static ConstructedTlvDataObject restoreAsn1SignatureStructure(byte [] signatureData){ int length = signatureData.length / 2; BigInteger r = new BigInteger(Arrays.copyOfRange(signatureData, 0, length)); BigInteger s = new BigInteger(Arrays.copyOfRange(signatureData, length, signatureData.length)); TlvDataObjectContainer integers = new TlvDataObjectContainer(); PrimitiveTlvDataObject integerRObject = new PrimitiveTlvDataObject(new TlvTag(Asn1.INTEGER), getPadded(r)); PrimitiveTlvDataObject integerSObject = new PrimitiveTlvDataObject(new TlvTag(Asn1.INTEGER), getPadded(s)); integers.addTlvDataObject(integerRObject); integers.addTlvDataObject(integerSObject); ConstructedTlvDataObject signatureObject = new ConstructedTlvDataObject(new TlvTag(Asn1.SEQUENCE), integers); return signatureObject; } /** * Restore a public key object from its ASN.1 representation. * @param publicKeyData the ASN.1 encoded key * @param paramSpec the {@link ECParameterSpec} to use * @return the {@link PublicKey} object * @throws GeneralSecurityException */ public static ECPublicKey parsePublicKeyEc(ConstructedTlvDataObject publicKeyData, ECParameterSpec paramSpec) throws GeneralSecurityException { TlvDataObject publicPointData = publicKeyData.getTlvDataObject(TlvConstants.TAG_86); ECPoint publicPoint = DomainParameterSetEcdh .reconstructPoint(publicPointData.getValueField()); ECPublicKeySpec keySpec = new ECPublicKeySpec(publicPoint, paramSpec); return (ECPublicKey) KeyFactory.getInstance("EC", Crypto.getCryptoProvider()) .generatePublic(keySpec); } /** * Restore the domain parameters form their ASN.1 representation. * @param publicKeyData the ASN.1 encoded key * @return the {@link ECParameterSpec} object * @throws GeneralSecurityException */ public static ECParameterSpec parseParameterSpecEc(ConstructedTlvDataObject publicKeyData){ TlvDataObject modulusData = publicKeyData.getTlvDataObject(TlvConstants.TAG_81); TlvDataObject firstCoefficientData = publicKeyData.getTlvDataObject(TlvConstants.TAG_82); TlvDataObject secondCoefficientData = publicKeyData.getTlvDataObject(TlvConstants.TAG_83); TlvDataObject basePointData = publicKeyData.getTlvDataObject(TlvConstants.TAG_84); TlvDataObject orderOfBasePointData = publicKeyData.getTlvDataObject(TlvConstants.TAG_85); TlvDataObject cofactorData = publicKeyData.getTlvDataObject(TlvConstants.TAG_87); ECField field = new ECFieldFp(new BigInteger(1, modulusData.getValueField())); EllipticCurve curve = new EllipticCurve(field, new BigInteger(1, firstCoefficientData.getValueField()), new BigInteger(1, secondCoefficientData.getValueField())); ECPoint basePoint = DomainParameterSetEcdh .reconstructPoint(basePointData.getValueField()); return new ECParameterSpec(curve, basePoint, new BigInteger(1, orderOfBasePointData.getValueField()), Utils.getIntFromUnsignedByteArray(cofactorData.getValueField())); } /** * This method recreates a {@link KeyPair} based on the provided domain parameter id and raw Byte arrays for public and private key. * @param standDomParamId the domain parameter id for standardized domain parameters * @param publicKeyData the raw public key data * @param privateKeyData the raw private key data * @return the reconstructed key pair */ public static KeyPair reconstructKeyPair(int standDomParamId, byte[] publicKeyData, byte[] privateKeyData) { return reconstructKeyPair(StandardizedDomainParameters.getDomainParameterSetById(standDomParamId), publicKeyData, privateKeyData); } /** * This method recreates a {@link KeyPair} based on the provided {@link DomainParameterSet} and raw Byte arrays for public and private key. * @param domParams the domain parameters to be used * @param publicKeyData the raw public key data * @param privateKeyData the raw private key data * @return the reconstructed key pair */ public static KeyPair reconstructKeyPair(DomainParameterSet domParams, byte[] publicKeyData, byte[] privateKeyData) { PublicKey publicKey = domParams.reconstructPublicKey(publicKeyData); PrivateKey privateKey = domParams.reconstructPrivateKey(privateKeyData); return new KeyPair(publicKey, privateKey); } /** * This method padds the given data to the given block size * @param unpaddedData the data to be padded * @param blockSize the block size * @return the padded data */ public static byte[] padData(byte[] unpaddedData, int blockSize) { /* +1 for mandatory padding byte 0x80 */ int overlap = (unpaddedData.length + 1) % blockSize; int nrOfZeros = blockSize - overlap; if (overlap == 0) { //input plus padding byte already matches BlockSize nrOfZeros = 0; } byte[] paddingZeros = new byte[nrOfZeros]; Arrays.fill(paddingZeros, (byte) 0x00); return Utils.concatByteArrays(unpaddedData, new byte[]{(byte) 0x80}, paddingZeros); } }