refactor decryptSessionKey
This commit is contained in:
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user