From b78954fc16e79d6910ef9b6c781faf755e89a158 Mon Sep 17 00:00:00 2001 From: Vincent Breitmoser Date: Thu, 10 Sep 2015 21:44:15 +0200 Subject: [PATCH] add support for signed-only data in the backend (#1507) --- .../operations/results/OperationResult.java | 1 + .../pgp/PgpDecryptVerifyOperation.java | 839 ++++++++++-------- OpenKeychain/src/main/res/values/strings.xml | 1 + 3 files changed, 447 insertions(+), 394 deletions(-) diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java index f213b1aad..46852d783 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java @@ -631,6 +631,7 @@ public abstract class OperationResult implements Parcelable { MSG_DC_TRAIL_SYM (LogLevel.DEBUG, R.string.msg_dc_trail_sym), MSG_DC_TRAIL_UNKNOWN (LogLevel.DEBUG, R.string.msg_dc_trail_unknown), MSG_DC_UNLOCKING (LogLevel.INFO, R.string.msg_dc_unlocking), + MSG_DC_INSECURE_ENCRYPTION_KEY (LogLevel.WARN, R.string.msg_dc_insecure_encryption_key), MSG_DC_INSECURE_SYMMETRIC_ENCRYPTION_ALGO(LogLevel.WARN, R.string.msg_dc_insecure_symmetric_encryption_algo), MSG_DC_INSECURE_HASH_ALGO(LogLevel.ERROR, R.string.msg_dc_insecure_hash_algo), MSG_DC_INSECURE_MDC_MISSING(LogLevel.ERROR, R.string.msg_dc_insecure_mdc_missing), diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyOperation.java index dd30156f9..a538c9bd1 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyOperation.java @@ -313,6 +313,27 @@ public class PgpDecryptVerifyOperation extends BaseOperation it = enc.getEncryptedDataObjects(); - boolean asymmetricPacketFound = false; - boolean symmetricPacketFound = false; - boolean anyPacketFound = false; - // If the input stream is armored, and there is a charset specified, take a note for later // https://tools.ietf.org/html/rfc4880#page56 String charset = null; @@ -375,261 +368,51 @@ public class PgpDecryptVerifyOperation extends BaseOperation 0) { - // Log.d(Constants.TAG, "read bytes: " + length); - if (out != null) { - out.write(buffer, 0, length); - } - - // update signature buffer if signature is also present - if (signature != null) { - signature.update(buffer, 0, length); - } - - alreadyWritten += length; - if (wholeSize > 0) { - long progress = 100 * alreadyWritten / wholeSize; - // stop at 100% for wrong file sizes... - if (progress > 100) { - progress = 100; - } - progressScaler.setProgress((int) progress, 100); - } - // TODO: slow annealing to fake a progress? + log.add(LogType.MSG_DC_CLEAR_META_SIZE_UNKNOWN, indent + 1); } metadata = new OpenPgpMetadata( originalFilename, mimeType, literalData.getModificationTime().getTime(), - alreadyWritten); + originalSize == null ? 0 : originalSize); - if (signature != null) { - updateProgress(R.string.progress_verifying_signature, 90, 100); - log.add(LogType.MSG_DC_CLEAR_SIGNATURE_CHECK, indent); - - PGPSignatureList signatureList = (PGPSignatureList) plainFact.nextObject(); - PGPSignature messageSignature = signatureList.get(signatureIndex); - - // TODO: what about binary signatures? - - // Verify signature - boolean validSignature = signature.verify(messageSignature); - if (validSignature) { - log.add(LogType.MSG_DC_CLEAR_SIGNATURE_OK, indent + 1); - } else { - log.add(LogType.MSG_DC_CLEAR_SIGNATURE_BAD, indent + 1); - } - - // check for insecure hash algorithms - if (!PgpSecurityConstants.isSecureHashAlgorithm(signature.getHashAlgorithm())) { - log.add(LogType.MSG_DC_INSECURE_HASH_ALGO, indent + 1); - signatureResultBuilder.setInsecure(true); - } - - signatureResultBuilder.setValidSignature(validSignature); - } - - indent -= 1; - } else { - // If there is no literalData, we don't have any metadata - metadata = null; + log.add(LogType.MSG_DC_OK_META_ONLY, indent); + DecryptVerifyResult result = + new DecryptVerifyResult(DecryptVerifyResult.RESULT_OK, log); + result.setCharset(charset); + result.setDecryptionMetadata(metadata); + return result; } - if (encryptedData.isIntegrityProtected()) { + int endProgress; + if (signature != null) { + endProgress = 90; + } else if (esResult != null && esResult.encryptedData.isIntegrityProtected()) { + endProgress = 95; + } else { + endProgress = 100; + } + ProgressScaler progressScaler = + new ProgressScaler(mProgressable, currentProgress, endProgress, 100); + + InputStream dataIn = literalData.getInputStream(); + + long alreadyWritten = 0; + long wholeSize = 0; // TODO inputData.getSize() - inputData.getStreamPosition(); + int length; + byte[] buffer = new byte[1 << 16]; + while ((length = dataIn.read(buffer)) > 0) { + // Log.d(Constants.TAG, "read bytes: " + length); + if (out != null) { + out.write(buffer, 0, length); + } + + // update signature buffer if signature is also present + if (signature != null) { + signature.update(buffer, 0, length); + } + + alreadyWritten += length; + if (wholeSize > 0) { + long progress = 100 * alreadyWritten / wholeSize; + // stop at 100% for wrong file sizes... + if (progress > 100) { + progress = 100; + } + progressScaler.setProgress((int) progress, 100); + } + // TODO: slow annealing to fake a progress? + } + + metadata = new OpenPgpMetadata( + originalFilename, mimeType, literalData.getModificationTime().getTime(), alreadyWritten); + + if (signature != null) { + updateProgress(R.string.progress_verifying_signature, 90, 100); + log.add(LogType.MSG_DC_CLEAR_SIGNATURE_CHECK, indent); + + PGPSignatureList signatureList = (PGPSignatureList) plainFact.nextObject(); + PGPSignature messageSignature = signatureList.get(signatureIndex); + + // Verify signature + boolean validSignature = signature.verify(messageSignature); + if (validSignature) { + log.add(LogType.MSG_DC_CLEAR_SIGNATURE_OK, indent + 1); + } else { + log.add(LogType.MSG_DC_CLEAR_SIGNATURE_BAD, indent + 1); + } + + // check for insecure hash algorithms + if (!PgpSecurityConstants.isSecureHashAlgorithm(signature.getHashAlgorithm())) { + log.add(LogType.MSG_DC_INSECURE_HASH_ALGO, indent + 1); + signatureResultBuilder.setInsecure(true); + } + + signatureResultBuilder.setValidSignature(validSignature); + } + + indent -= 1; + + if (esResult != null && esResult.encryptedData.isIntegrityProtected()) { updateProgress(R.string.progress_verifying_integrity, 95, 100); - if (encryptedData.verify()) { + if (esResult.encryptedData.verify()) { log.add(LogType.MSG_DC_INTEGRITY_CHECK_OK, indent); } else { log.add(LogType.MSG_DC_ERROR_INTEGRITY_CHECK, indent); @@ -874,11 +656,280 @@ public class PgpDecryptVerifyOperation extends BaseOperation it = enc.getEncryptedDataObjects(); + + // go through all objects and find one we can decrypt + while (it.hasNext()) { + Object obj = it.next(); + if (obj instanceof PGPPublicKeyEncryptedData) { + anyPacketFound = true; + + currentProgress += 2; + updateProgress(R.string.progress_finding_key, currentProgress, 100); + + PGPPublicKeyEncryptedData encData = (PGPPublicKeyEncryptedData) obj; + long subKeyId = encData.getKeyID(); + + log.add(LogType.MSG_DC_ASYM, indent, + KeyFormattingUtils.convertKeyIdToHex(subKeyId)); + + CanonicalizedSecretKeyRing secretKeyRing; + try { + // get actual keyring object based on master key id + secretKeyRing = mProviderHelper.getCanonicalizedSecretKeyRing( + KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(subKeyId) + ); + } catch (ProviderHelper.NotFoundException e) { + // continue with the next packet in the while loop + log.add(LogType.MSG_DC_ASKIP_NO_KEY, indent + 1); + continue; + } + if (secretKeyRing == null) { + // continue with the next packet in the while loop + log.add(LogType.MSG_DC_ASKIP_NO_KEY, indent + 1); + continue; + } + + // allow only specific keys for decryption? + if (input.getAllowedKeyIds() != null) { + long masterKeyId = secretKeyRing.getMasterKeyId(); + Log.d(Constants.TAG, "encData.getKeyID(): " + subKeyId); + Log.d(Constants.TAG, "mAllowedKeyIds: " + input.getAllowedKeyIds()); + Log.d(Constants.TAG, "masterKeyId: " + masterKeyId); + + if (!input.getAllowedKeyIds().contains(masterKeyId)) { + // this key is in our db, but NOT allowed! + // continue with the next packet in the while loop + result.skippedDisallowedKey = true; + log.add(LogType.MSG_DC_ASKIP_NOT_ALLOWED, indent + 1); + continue; + } + } + + // get subkey which has been used for this encryption packet + secretEncryptionKey = secretKeyRing.getSecretKey(subKeyId); + if (secretEncryptionKey == null) { + // should actually never happen, so no need to be more specific. + log.add(LogType.MSG_DC_ASKIP_NO_KEY, indent + 1); + continue; + } + + /* secret key exists in database and is allowed! */ + asymmetricPacketFound = true; + + encryptedDataAsymmetric = encData; + + if (secretEncryptionKey.getSecretKeyType() == SecretKeyType.DIVERT_TO_CARD) { + passphrase = null; + } else if (cryptoInput.hasPassphrase()) { + passphrase = cryptoInput.getPassphrase(); + } else { + // if no passphrase was explicitly set try to get it from the cache service + try { + // returns "" if key has no passphrase + passphrase = getCachedPassphrase(subKeyId); + log.add(LogType.MSG_DC_PASS_CACHED, indent + 1); + } catch (PassphraseCacheInterface.NoSecretKeyException e) { + log.add(LogType.MSG_DC_ERROR_NO_KEY, indent + 1); + return result.with(new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log)); + } + + // if passphrase was not cached, return here indicating that a passphrase is missing! + if (passphrase == null) { + log.add(LogType.MSG_DC_PENDING_PASSPHRASE, indent + 1); + return result.with(new DecryptVerifyResult(log, + RequiredInputParcel.createRequiredDecryptPassphrase( + secretKeyRing.getMasterKeyId(), secretEncryptionKey.getKeyId()), + cryptoInput)); + } + } + + // check for insecure encryption key + if ( ! PgpSecurityConstants.isSecureKey(secretEncryptionKey)) { + log.add(LogType.MSG_DC_INSECURE_KEY, indent + 1); + result.insecureEncryptionKey = true; + } + + // break out of while, only decrypt the first packet where we have a key + break; + + } else if (obj instanceof PGPPBEEncryptedData) { + anyPacketFound = true; + + log.add(LogType.MSG_DC_SYM, indent); + + if (!input.isAllowSymmetricDecryption()) { + log.add(LogType.MSG_DC_SYM_SKIP, indent + 1); + continue; + } + + /* + * When mAllowSymmetricDecryption == true and we find a data packet here, + * we do not search for other available asymmetric packets! + */ + symmetricPacketFound = true; + + encryptedDataSymmetric = (PGPPBEEncryptedData) obj; + + // if no passphrase is given, return here + // indicating that a passphrase is missing! + if (!cryptoInput.hasPassphrase()) { + + try { + passphrase = getCachedPassphrase(key.symmetric); + log.add(LogType.MSG_DC_PASS_CACHED, indent + 1); + } catch (PassphraseCacheInterface.NoSecretKeyException e) { + // nvm + } + + if (passphrase == null) { + log.add(LogType.MSG_DC_PENDING_PASSPHRASE, indent + 1); + return result.with(new DecryptVerifyResult(log, + RequiredInputParcel.createRequiredSymmetricPassphrase(), + cryptoInput)); + } + + } else { + passphrase = cryptoInput.getPassphrase(); + } + + // break out of while, only decrypt the first packet + break; + } + } + + // More data, just acknowledge and ignore. + while (it.hasNext()) { + Object obj = it.next(); + if (obj instanceof PGPPublicKeyEncryptedData) { + PGPPublicKeyEncryptedData encData = (PGPPublicKeyEncryptedData) obj; + long subKeyId = encData.getKeyID(); + log.add(LogType.MSG_DC_TRAIL_ASYM, indent, + KeyFormattingUtils.convertKeyIdToHex(subKeyId)); + } else if (obj instanceof PGPPBEEncryptedData) { + log.add(LogType.MSG_DC_TRAIL_SYM, indent); + } else { + log.add(LogType.MSG_DC_TRAIL_UNKNOWN, indent); + } + } + + // we made sure above one of these two would be true + if (symmetricPacketFound) { + currentProgress += 2; + updateProgress(R.string.progress_preparing_streams, currentProgress, 100); + + PGPDigestCalculatorProvider digestCalcProvider = new JcaPGPDigestCalculatorProviderBuilder() + .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(); + PBEDataDecryptorFactory decryptorFactory = new JcePBEDataDecryptorFactoryBuilder( + digestCalcProvider).setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build( + passphrase.getCharArray()); + + try { + result.cleartextStream = encryptedDataSymmetric.getDataStream(decryptorFactory); + } catch (PGPDataValidationException e) { + log.add(LogType.MSG_DC_ERROR_SYM_PASSPHRASE, indent + 1); + return result.with(new DecryptVerifyResult(log, + RequiredInputParcel.createRequiredSymmetricPassphrase(), cryptoInput)); + } + + result.encryptedData = encryptedDataSymmetric; + + result.symmetricEncryptionAlgo = encryptedDataSymmetric.getSymmetricAlgorithm(decryptorFactory); + } else if (asymmetricPacketFound) { + currentProgress += 2; + updateProgress(R.string.progress_extracting_key, currentProgress, 100); + + try { + log.add(LogType.MSG_DC_UNLOCKING, indent + 1); + if (!secretEncryptionKey.unlock(passphrase)) { + log.add(LogType.MSG_DC_ERROR_BAD_PASSPHRASE, indent + 1); + return result.with(new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log)); + } + } catch (PgpGeneralException e) { + log.add(LogType.MSG_DC_ERROR_EXTRACT_KEY, indent + 1); + return result.with(new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log)); + } + + currentProgress += 2; + updateProgress(R.string.progress_preparing_streams, currentProgress, 100); + + CachingDataDecryptorFactory decryptorFactory + = secretEncryptionKey.getCachingDecryptorFactory(cryptoInput); + + // special case: if the decryptor does not have a session key cached for this encrypted + // data, and can't actually decrypt on its own, return a pending intent + if (!decryptorFactory.canDecrypt() + && !decryptorFactory.hasCachedSessionData(encryptedDataAsymmetric)) { + + log.add(LogType.MSG_DC_PENDING_NFC, indent + 1); + return result.with(new DecryptVerifyResult(log, RequiredInputParcel.createNfcDecryptOperation( + secretEncryptionKey.getRing().getMasterKeyId(), + secretEncryptionKey.getKeyId(), encryptedDataAsymmetric.getSessionKey()[0] + ), cryptoInput)); + + } + + try { + result.cleartextStream = encryptedDataAsymmetric.getDataStream(decryptorFactory); + } catch (PGPKeyValidationException | ArrayIndexOutOfBoundsException e) { + log.add(LogType.MSG_DC_ERROR_CORRUPT_DATA, indent + 1); + return result.with(new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log)); + } + + result.symmetricEncryptionAlgo = encryptedDataAsymmetric.getSymmetricAlgorithm(decryptorFactory); + result.encryptedData = encryptedDataAsymmetric; + + cryptoInput.addCryptoData(decryptorFactory.getCachedSessionKeys()); + + } else { + // there wasn't even any useful data + if (!anyPacketFound) { + log.add(LogType.MSG_DC_ERROR_NO_DATA, indent + 1); + return result.with(new DecryptVerifyResult(DecryptVerifyResult.RESULT_NO_DATA, log)); + } + // there was data but key wasn't allowed + if (result.skippedDisallowedKey) { + log.add(LogType.MSG_DC_ERROR_NO_KEY, indent + 1); + return result.with(new DecryptVerifyResult(DecryptVerifyResult.RESULT_KEY_DISALLOWED, log)); + } + // no packet has been found where we have the corresponding secret key in our db + log.add(LogType.MSG_DC_ERROR_NO_KEY, indent + 1); + return result.with(new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log)); + } + return result; } diff --git a/OpenKeychain/src/main/res/values/strings.xml b/OpenKeychain/src/main/res/values/strings.xml index 52632c58d..e6d607591 100644 --- a/OpenKeychain/src/main/res/values/strings.xml +++ b/OpenKeychain/src/main/res/values/strings.xml @@ -1187,6 +1187,7 @@ "Encountered trailing, symmetrically encrypted data" "Encountered trailing data of unknown type" "Unlocking secret key" + "Insecure encryption key was used! This can happen because the key is old, or from an attack." "Insecure encryption algorithm has been used! This can happen because the application is out of date, or from an attack." "Insecure hash algorithm has been used! This can happen because the application is out of date, or from an attack." "Missing the Modification Detection Code (MDC) packet! This can happen because the encrypting application is out of date, or from a downgrade attack."