refactor decryptSessionKey

This commit is contained in:
Vincent Breitmoser
2018-01-12 02:04:58 +01:00
parent 0ab71ea498
commit 139735f0e1
3 changed files with 106 additions and 100 deletions

View File

@@ -24,142 +24,151 @@ import org.bouncycastle.util.encoders.Hex;
import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKey; import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKey;
/** This class implements the PSO:DECIPHER operation, as specified in OpenPGP card spec / 7.2.11 (p52 in v3.0.1).
*
* See https://www.g10code.com/docs/openpgp-card-3.0.pdf
*/
public class PsoDecryptUseCase { public class PsoDecryptUseCase {
private final SecurityTokenConnection connection; private final SecurityTokenConnection connection;
private final JcaKeyFingerprintCalculator fingerprintCalculator; private final JcaKeyFingerprintCalculator fingerprintCalculator;
public static PsoDecryptUseCase create(SecurityTokenConnection connection) { public static PsoDecryptUseCase create(SecurityTokenConnection connection) {
return new PsoDecryptUseCase(connection); return new PsoDecryptUseCase(connection, new JcaKeyFingerprintCalculator());
} }
private PsoDecryptUseCase(SecurityTokenConnection connection) { private PsoDecryptUseCase(SecurityTokenConnection connection,
JcaKeyFingerprintCalculator jcaKeyFingerprintCalculator) {
this.connection = connection; this.connection = connection;
this.fingerprintCalculator = new JcaKeyFingerprintCalculator(); this.fingerprintCalculator = jcaKeyFingerprintCalculator;
} }
public byte[] decryptSessionKey(@NonNull byte[] encryptedSessionKey, public byte[] verifyAndDecryptSessionKey(@NonNull byte[] encryptedSessionKeyMpi, CanonicalizedPublicKey publicKey)
CanonicalizedPublicKey publicKey)
throws IOException { throws IOException {
final KeyFormat kf = connection.getOpenPgpCapabilities().getFormatForKeyType(KeyType.ENCRYPT);
connection.verifyPinForOther(); connection.verifyPinForOther();
byte[] data; KeyFormat kf = connection.getOpenPgpCapabilities().getFormatForKeyType(KeyType.ENCRYPT);
byte[] dataLen;
int pLen = 0;
X9ECParameters x9Params;
switch (kf.keyFormatType()) { switch (kf.keyFormatType()) {
case RSAKeyFormatType: case RSAKeyFormatType:
data = Arrays.copyOfRange(encryptedSessionKey, 2, encryptedSessionKey.length); return decryptSessionKeyRsa(encryptedSessionKeyMpi);
if (data[0] != 0) {
data = Arrays.prepend(data, (byte) 0x00);
}
break;
case ECKeyFormatType: case ECKeyFormatType:
pLen = ((((encryptedSessionKey[0] & 0xff) << 8) + (encryptedSessionKey[1] & 0xff)) + 7) / 8; return decryptSessionKeyEcdh(encryptedSessionKeyMpi, (ECKeyFormat) kf, publicKey);
data = new byte[pLen];
System.arraycopy(encryptedSessionKey, 2, data, 0, pLen);
final ECKeyFormat eckf = (ECKeyFormat) kf;
x9Params = NISTNamedCurves.getByOID(eckf.getCurveOID());
final ECPoint p = x9Params.getCurve().decodePoint(data);
if (!p.isValid()) {
throw new CardException("Invalid EC point!");
}
data = p.getEncoded(false);
if (data.length < 128) {
dataLen = new byte[]{(byte) data.length};
} else {
dataLen = new byte[]{(byte) 0x81, (byte) data.length};
}
data = Arrays.concatenate(Hex.decode("86"), dataLen, data);
if (data.length < 128) {
dataLen = new byte[]{(byte) data.length};
} else {
dataLen = new byte[]{(byte) 0x81, (byte) data.length};
}
data = Arrays.concatenate(Hex.decode("7F49"), dataLen, data);
if (data.length < 128) {
dataLen = new byte[]{(byte) data.length};
} else {
dataLen = new byte[]{(byte) 0x81, (byte) data.length};
}
data = Arrays.concatenate(Hex.decode("A6"), dataLen, data);
break;
default: default:
throw new CardException("Unknown encryption key type!"); throw new CardException("Unknown encryption key type!");
} }
}
CommandApdu command = connection.getCommandFactory().createDecipherCommand(data); private byte[] decryptSessionKeyRsa(byte[] encryptedSessionKeyMpi) throws IOException {
int mpiLength = getMpiLength(encryptedSessionKeyMpi);
if (mpiLength != encryptedSessionKeyMpi.length - 2) {
throw new IOException("Malformed RSA session key!");
}
byte[] psoDecipherPayload = new byte[mpiLength + 1];
psoDecipherPayload[0] = (byte) 0x00; // RSA Padding Indicator Byte
System.arraycopy(encryptedSessionKeyMpi, 2, psoDecipherPayload, 1, mpiLength);
CommandApdu command = connection.getCommandFactory().createDecipherCommand(psoDecipherPayload);
ResponseApdu response = connection.communicate(command); ResponseApdu response = connection.communicate(command);
if (!response.isSuccess()) { if (!response.isSuccess()) {
throw new CardException("Deciphering with Security token failed on receive", response.getSw()); throw new CardException("Deciphering with Security token failed on receive", response.getSw());
} }
switch (connection.getOpenPgpCapabilities().getFormatForKeyType(KeyType.ENCRYPT).keyFormatType()) { return response.getData();
case RSAKeyFormatType: }
return response.getData();
/* From 3.x OpenPGP card specification : private byte[] decryptSessionKeyEcdh(byte[] encryptedSessionKeyMpi, ECKeyFormat eckf, CanonicalizedPublicKey publicKey)
In case of ECDH the card supports a partial decrypt only. throws IOException {
With its own private key and the given public key the card calculates a shared secret int mpiLength = getMpiLength(encryptedSessionKeyMpi);
in compliance with the Elliptic Curve Key Agreement Scheme from Diffie-Hellman. byte[] encryptedPoint = Arrays.copyOfRange(encryptedSessionKeyMpi, 2, mpiLength);
The shared secret is returned in the response, all other calculation for deciphering
are done outside of the card.
The shared secret obtained is a KEK (Key Encryption Key) that is used to wrap the X9ECParameters x9Params = NISTNamedCurves.getByOID(eckf.getCurveOID());
session key. ECPoint p = x9Params.getCurve().decodePoint(encryptedPoint);
if (!p.isValid()) {
throw new CardException("Invalid EC point!");
}
From rfc6637#section-13 : byte[] psoDecipherPayload = p.getEncoded(false);
This document explicitly discourages the use of algorithms other than AES as a KEK algorithm.
*/
case ECKeyFormatType:
data = response.getData();
final byte[] keyEnc = new byte[encryptedSessionKey[pLen + 2]]; byte[] dataLen;
if (psoDecipherPayload.length < 128) {
dataLen = new byte[]{(byte) psoDecipherPayload.length};
} else {
dataLen = new byte[]{(byte) 0x81, (byte) psoDecipherPayload.length};
}
psoDecipherPayload = Arrays.concatenate(Hex.decode("86"), dataLen, psoDecipherPayload);
System.arraycopy(encryptedSessionKey, 2 + pLen + 1, keyEnc, 0, keyEnc.length); if (psoDecipherPayload.length < 128) {
dataLen = new byte[]{(byte) psoDecipherPayload.length};
} else {
dataLen = new byte[]{(byte) 0x81, (byte) psoDecipherPayload.length};
}
psoDecipherPayload = Arrays.concatenate(Hex.decode("7F49"), dataLen, psoDecipherPayload);
try { if (psoDecipherPayload.length < 128) {
final MessageDigest kdf = MessageDigest.getInstance(MessageDigestUtils.getDigestName(publicKey.getSecurityTokenHashAlgorithm())); dataLen = new byte[]{(byte) psoDecipherPayload.length};
} else {
dataLen = new byte[]{(byte) 0x81, (byte) psoDecipherPayload.length};
}
psoDecipherPayload = Arrays.concatenate(Hex.decode("A6"), dataLen, psoDecipherPayload);
kdf.update(new byte[]{(byte) 0, (byte) 0, (byte) 0, (byte) 1}); CommandApdu command = connection.getCommandFactory().createDecipherCommand(psoDecipherPayload);
kdf.update(data); ResponseApdu response = connection.communicate(command);
kdf.update(publicKey.createUserKeyingMaterial(fingerprintCalculator));
final byte[] kek = kdf.digest(); if (!response.isSuccess()) {
final Cipher c = Cipher.getInstance("AESWrap"); throw new CardException("Deciphering with Security token failed on receive", response.getSw());
}
c.init(Cipher.UNWRAP_MODE, new SecretKeySpec(kek, 0, publicKey.getSecurityTokenSymmetricKeySize() / 8, "AES")); /* From 3.x OpenPGP card specification :
In case of ECDH the card supports a partial decrypt only.
With its own private key and the given public key the card calculates a shared secret
in compliance with the Elliptic Curve Key Agreement Scheme from Diffie-Hellman.
The shared secret is returned in the response, all other calculation for deciphering
are done outside of the card.
final Key paddedSessionKey = c.unwrap(keyEnc, "Session", Cipher.SECRET_KEY); The shared secret obtained is a KEK (Key Encryption Key) that is used to wrap the
session key.
Arrays.fill(kek, (byte) 0); From rfc6637#section-13 :
This document explicitly discourages the use of algorithms other than AES as a KEK algorithm.
*/
byte[] keyEncryptionKey = response.getData();
return PGPPad.unpadSessionData(paddedSessionKey.getEncoded()); final byte[] keyEnc = new byte[encryptedSessionKeyMpi[mpiLength + 2]];
} catch (NoSuchAlgorithmException e) {
throw new CardException("Unknown digest/encryption algorithm!");
} catch (NoSuchPaddingException e) {
throw new CardException("Unknown padding algorithm!");
} catch (PGPException e) {
throw new CardException(e.getMessage());
} catch (InvalidKeyException e) {
throw new CardException("Invalid KEK!");
}
default: System.arraycopy(encryptedSessionKeyMpi, 2 + mpiLength + 1, keyEnc, 0, keyEnc.length);
throw new CardException("Unknown encryption key type!");
try {
final MessageDigest kdf = MessageDigest.getInstance(MessageDigestUtils.getDigestName(publicKey.getSecurityTokenHashAlgorithm()));
kdf.update(new byte[]{(byte) 0, (byte) 0, (byte) 0, (byte) 1});
kdf.update(keyEncryptionKey);
kdf.update(publicKey.createUserKeyingMaterial(fingerprintCalculator));
byte[] kek = kdf.digest();
Cipher c = Cipher.getInstance("AESWrap");
c.init(Cipher.UNWRAP_MODE, new SecretKeySpec(kek, 0, publicKey.getSecurityTokenSymmetricKeySize() / 8, "AES"));
Key paddedSessionKey = c.unwrap(keyEnc, "Session", Cipher.SECRET_KEY);
Arrays.fill(kek, (byte) 0);
return PGPPad.unpadSessionData(paddedSessionKey.getEncoded());
} catch (NoSuchAlgorithmException e) {
throw new CardException("Unknown digest/encryption algorithm!");
} catch (NoSuchPaddingException e) {
throw new CardException("Unknown padding algorithm!");
} catch (PGPException e) {
throw new CardException(e.getMessage());
} catch (InvalidKeyException e) {
throw new CardException("Invalid KEK!");
} }
} }
private int getMpiLength(byte[] multiPrecisionInteger) {
return ((((multiPrecisionInteger[0] & 0xff) << 8) + (multiPrecisionInteger[1] & 0xff)) + 7) / 8;
}
} }

View File

@@ -212,7 +212,7 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenActivity {
for (int i = 0; i < mRequiredInput.mInputData.length; i++) { for (int i = 0; i < mRequiredInput.mInputData.length; i++) {
byte[] encryptedSessionKey = mRequiredInput.mInputData[i]; byte[] encryptedSessionKey = mRequiredInput.mInputData[i];
byte[] decryptedSessionKey = psoDecryptUseCase byte[] decryptedSessionKey = psoDecryptUseCase
.decryptSessionKey(encryptedSessionKey, publicKeyRing.getPublicKey(tokenKeyId)); .verifyAndDecryptSessionKey(encryptedSessionKey, publicKeyRing.getPublicKey(tokenKeyId));
mInputParcel = mInputParcel.withCryptoData(encryptedSessionKey, decryptedSessionKey); mInputParcel = mInputParcel.withCryptoData(encryptedSessionKey, decryptedSessionKey);
} }
break; break;

View File

@@ -6,9 +6,6 @@ import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.sufficientlysecure.keychain.KeychainTestRunner; import org.sufficientlysecure.keychain.KeychainTestRunner;
import org.sufficientlysecure.keychain.securitytoken.SecurityTokenInfo.TokenType;
import org.sufficientlysecure.keychain.securitytoken.SecurityTokenInfo.TransportType;
import org.sufficientlysecure.keychain.util.Passphrase;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import static org.mockito.Matchers.any; import static org.mockito.Matchers.any;
@@ -58,7 +55,7 @@ public class PsoDecryptUseCaseTest {
when(commandFactory.createDecipherCommand(any(byte[].class))).thenReturn(dummyCommandApdu); when(commandFactory.createDecipherCommand(any(byte[].class))).thenReturn(dummyCommandApdu);
when(securityTokenConnection.communicate(dummyCommandApdu)).thenReturn(dummyResponseApdu); when(securityTokenConnection.communicate(dummyCommandApdu)).thenReturn(dummyResponseApdu);
byte[] response = useCase.decryptSessionKey(RSA_ENC_SESSIONKEY_MPI, null); byte[] response = useCase.verifyAndDecryptSessionKey(RSA_ENC_SESSIONKEY_MPI, null);
assertArrayEquals(Hex.decode("01020304"), response); assertArrayEquals(Hex.decode("01020304"), response);
} }