diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/AutocryptPeerDataAccessObject.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/AutocryptPeerDataAccessObject.java index d4fd4645c..4d5987423 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/AutocryptPeerDataAccessObject.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/AutocryptPeerDataAccessObject.java @@ -85,13 +85,30 @@ public class AutocryptPeerDataAccessObject { return null; } - public Date getLastUpdateForAutocryptPeer(String autocryptId) { + public Date getLastSeen(String autocryptId) { Cursor cursor = mQueryInterface.query(ApiAutocryptPeer.buildByPackageNameAndAutocryptId(packageName, autocryptId), null, null, null, null); try { if (cursor != null && cursor.moveToFirst()) { - long lastUpdated = cursor.getColumnIndex(ApiAutocryptPeer.LAST_UPDATED); + long lastUpdated = cursor.getColumnIndex(ApiAutocryptPeer.LAST_SEEN); + return new Date(lastUpdated); + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + return null; + } + + public Date getLastSeenKey(String autocryptId) { + Cursor cursor = mQueryInterface.query(ApiAutocryptPeer.buildByPackageNameAndAutocryptId(packageName, autocryptId), + null, null, null, null); + + try { + if (cursor != null && cursor.moveToFirst()) { + long lastUpdated = cursor.getColumnIndex(ApiAutocryptPeer.LAST_SEEN_KEY); return new Date(lastUpdated); } } finally { @@ -103,14 +120,36 @@ public class AutocryptPeerDataAccessObject { } public void setMasterKeyIdForAutocryptPeer(String autocryptId, long masterKeyId, Date date) { - Date lastUpdated = getLastUpdateForAutocryptPeer(autocryptId); + Date lastUpdated = getLastSeen(autocryptId); if (lastUpdated != null && lastUpdated.after(date)) { throw new IllegalArgumentException("Database entry was newer than the one to be inserted! Cannot backdate"); } ContentValues cv = new ContentValues(); cv.put(ApiAutocryptPeer.MASTER_KEY_ID, masterKeyId); - cv.put(ApiAutocryptPeer.LAST_UPDATED, date.getTime()); + cv.put(ApiAutocryptPeer.LAST_SEEN_KEY, date.getTime()); + mQueryInterface.update(ApiAutocryptPeer.buildByPackageNameAndAutocryptId(packageName, autocryptId), cv, null, null); + } + + public void updateToResetState(String autocryptId, Date effectiveDate) { + updateAutocryptState(autocryptId, effectiveDate, ApiAutocryptPeer.RESET); + } + + public void updateToMutualState(String autocryptId, Date effectiveDate, long masterKeyId) { + setMasterKeyIdForAutocryptPeer(autocryptId, masterKeyId, effectiveDate); + updateAutocryptState(autocryptId, effectiveDate, ApiAutocryptPeer.MUTUAL); + } + + public void updateToAvailableState(String autocryptId, Date effectiveDate, long masterKeyId) { + setMasterKeyIdForAutocryptPeer(autocryptId, masterKeyId, effectiveDate); + updateAutocryptState(autocryptId, effectiveDate, ApiAutocryptPeer.AVAILABLE); + } + + private void updateAutocryptState(String autocryptId, Date date, int status) { + ContentValues cv = new ContentValues(); + cv.put(ApiAutocryptPeer.MASTER_KEY_ID, (Integer) null); + cv.put(ApiAutocryptPeer.LAST_SEEN, date.getTime()); + cv.put(ApiAutocryptPeer.STATUS, status); mQueryInterface.update(ApiAutocryptPeer.buildByPackageNameAndAutocryptId(packageName, autocryptId), cv, null, null); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java index 3a67994a0..384321329 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java @@ -98,7 +98,9 @@ public class KeychainContract { interface ApiAutocryptPeerColumns { String PACKAGE_NAME = "package_name"; String IDENTIFIER = "identifier"; - String LAST_UPDATED = "last_updated"; + String LAST_SEEN = "last_updated"; + String LAST_SEEN_KEY = "last_seen_key"; + String STATUS = "status"; String MASTER_KEY_ID = "master_key_id"; } @@ -349,6 +351,11 @@ public class KeychainContract { public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon() .appendPath(BASE_AUTOCRYPT_PEERS).build(); + public static final int RESET = 0; + public static final int GOSSIP = 1; + public static final int AVAILABLE = 2; + public static final int MUTUAL = 3; + public static Uri buildByKeyUri(Uri uri) { return CONTENT_URI.buildUpon().appendPath(PATH_BY_KEY_ID).appendPath(uri.getPathSegments().get(1)).build(); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java index 9e6878876..6466d4293 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java @@ -162,8 +162,10 @@ public class KeychainDatabase extends SQLiteOpenHelper { "CREATE TABLE IF NOT EXISTS " + Tables.API_AUTOCRYPT_PEERS + " (" + ApiAutocryptPeerColumns.PACKAGE_NAME + " TEXT NOT NULL, " + ApiAutocryptPeerColumns.IDENTIFIER + " TEXT NOT NULL, " - + ApiAutocryptPeerColumns.LAST_UPDATED + " INTEGER NOT NULL, " - + ApiAutocryptPeerColumns.MASTER_KEY_ID + " INTEGER NOT NULL, " + + ApiAutocryptPeerColumns.LAST_SEEN + " INTEGER NOT NULL, " + + ApiAutocryptPeerColumns.LAST_SEEN_KEY + " INTEGER NOT NULL, " + + ApiAutocryptPeerColumns.STATUS + " INTEGER NOT NULL, " + + ApiAutocryptPeerColumns.MASTER_KEY_ID + " INTEGER NULL, " + "PRIMARY KEY(" + ApiAutocryptPeerColumns.PACKAGE_NAME + ", " + ApiAutocryptPeerColumns.IDENTIFIER + "), " + "FOREIGN KEY(" + ApiAutocryptPeerColumns.PACKAGE_NAME + ") REFERENCES " diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainExternalContract.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainExternalContract.java index 1253551fd..4ef39f896 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainExternalContract.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainExternalContract.java @@ -26,6 +26,7 @@ import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAutocryptPee public class KeychainExternalContract { + public static final int KEY_STATUS_UNAVAILABLE = 0; public static final int KEY_STATUS_UNVERIFIED = 1; public static final int KEY_STATUS_VERIFIED = 2; @@ -35,6 +36,7 @@ public class KeychainExternalContract { private static final Uri BASE_CONTENT_URI_EXTERNAL = Uri .parse("content://" + CONTENT_AUTHORITY_EXTERNAL); public static final String BASE_EMAIL_STATUS = "email_status"; + public static final String BASE_AUTOCRYPT_PEER_STATUS = "autocrypt_status"; public static final String BASE_AUTOCRYPT_PEERS = "autocrypt_peers"; @@ -43,8 +45,6 @@ public class KeychainExternalContract { public static final String USER_ID = "user_id"; public static final String USER_ID_STATUS = "email_status"; public static final String MASTER_KEY_ID = "master_key_id"; - public static final String AUTOCRYPT_PEER_LAST_SEEN = "autocrypt_peer_last_seen"; - public static final String AUTOCRYPT_PEER_STATE = "autocrypt_peer_state"; public static final Uri CONTENT_URI = BASE_CONTENT_URI_EXTERNAL.buildUpon() .appendPath(BASE_EMAIL_STATUS).build(); @@ -53,6 +53,19 @@ public class KeychainExternalContract { "vnd.android.cursor.dir/vnd.org.sufficientlysecure.keychain.provider.email_status"; } + public static class AutocryptPeerStatus implements BaseColumns { + public static final String EMAIL_ADDRESS = "email_address"; + public static final String MASTER_KEY_ID = "master_key_id"; + public static final String AUTOCRYPT_PEER_LAST_SEEN = "autocrypt_peer_last_seen"; + public static final String AUTOCRYPT_PEER_STATUS = "autocrypt_peer_state"; + + public static final Uri CONTENT_URI = BASE_CONTENT_URI_EXTERNAL.buildUpon() + .appendPath(BASE_EMAIL_STATUS).build(); + + public static final String CONTENT_TYPE = + "vnd.android.cursor.dir/vnd.org.sufficientlysecure.keychain.provider.autocrypt_status"; + } + public static class ApiAutocryptPeer implements ApiAutocryptPeerColumns, BaseColumns { public static final Uri CONTENT_URI = BASE_CONTENT_URI_EXTERNAL.buildUpon() .appendPath(BASE_AUTOCRYPT_PEERS).build(); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java index 995240240..47d701722 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java @@ -676,7 +676,7 @@ public class KeychainProvider extends ContentProvider { projectionMap.put(ApiAutocryptPeer.PACKAGE_NAME, ApiAutocryptPeer.PACKAGE_NAME); projectionMap.put(ApiAutocryptPeer.IDENTIFIER, ApiAutocryptPeer.IDENTIFIER); projectionMap.put(ApiAutocryptPeer.MASTER_KEY_ID, ApiAutocryptPeer.MASTER_KEY_ID); - projectionMap.put(ApiAutocryptPeer.LAST_UPDATED, ApiAutocryptPeer.LAST_UPDATED); + projectionMap.put(ApiAutocryptPeer.LAST_SEEN, ApiAutocryptPeer.LAST_SEEN); qb.setProjectionMap(projectionMap); qb.setTables(Tables.API_AUTOCRYPT_PEERS); @@ -999,7 +999,7 @@ public class KeychainProvider extends ContentProvider { } case TRUST_IDS_BY_PACKAGE_NAME_AND_TRUST_ID: { Long masterKeyId = values.getAsLong(ApiAutocryptPeer.MASTER_KEY_ID); - long updateTime = values.getAsLong(ApiAutocryptPeer.LAST_UPDATED); + long updateTime = values.getAsLong(ApiAutocryptPeer.LAST_SEEN); if (masterKeyId == null) { throw new IllegalArgumentException("master_key_id must be a non-null value!"); } @@ -1009,7 +1009,7 @@ public class KeychainProvider extends ContentProvider { actualValues.put(ApiAutocryptPeer.PACKAGE_NAME, packageName); actualValues.put(ApiAutocryptPeer.IDENTIFIER, uri.getLastPathSegment()); actualValues.put(ApiAutocryptPeer.MASTER_KEY_ID, masterKeyId); - actualValues.put(ApiAutocryptPeer.LAST_UPDATED, updateTime); + actualValues.put(ApiAutocryptPeer.LAST_SEEN, updateTime); try { db.replace(Tables.API_AUTOCRYPT_PEERS, null, actualValues); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/KeychainExternalProvider.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/KeychainExternalProvider.java index b07384f9c..46bc56bdd 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/KeychainExternalProvider.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/KeychainExternalProvider.java @@ -48,6 +48,7 @@ import org.sufficientlysecure.keychain.provider.KeychainDatabase; import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables; import org.sufficientlysecure.keychain.provider.KeychainExternalContract; import org.sufficientlysecure.keychain.provider.KeychainExternalContract.ApiAutocryptPeer; +import org.sufficientlysecure.keychain.provider.KeychainExternalContract.AutocryptPeerStatus; import org.sufficientlysecure.keychain.provider.KeychainExternalContract.EmailStatus; import org.sufficientlysecure.keychain.provider.SimpleContentResolverInterface; import org.sufficientlysecure.keychain.util.Log; @@ -58,6 +59,8 @@ public class KeychainExternalProvider extends ContentProvider implements SimpleC private static final int AUTOCRYPT_PEER = 201; private static final int API_APPS = 301; private static final int API_APPS_BY_PACKAGE_NAME = 302; + private static final int AUTOCRYPT_PEER_STATUS = 401; + private static final int AUTOCRYPT_PEER_STATUS_INTERNAL = 402; public static final String TEMP_TABLE_QUERIED_ADDRESSES = "queried_addresses"; public static final String TEMP_TABLE_COLUMN_ADDRES = "address"; @@ -88,6 +91,9 @@ public class KeychainExternalProvider extends ContentProvider implements SimpleC matcher.addURI(authority, KeychainExternalContract.BASE_AUTOCRYPT_PEERS + "/*", AUTOCRYPT_PEER); + matcher.addURI(authority, KeychainExternalContract.BASE_AUTOCRYPT_PEER_STATUS, AUTOCRYPT_PEER_STATUS); + matcher.addURI(authority, KeychainExternalContract.BASE_AUTOCRYPT_PEER_STATUS + "/*", AUTOCRYPT_PEER_STATUS_INTERNAL); + // can only query status of calling app - for internal use only! matcher.addURI(KeychainContract.CONTENT_AUTHORITY, KeychainContract.BASE_API_APPS + "/*", API_APPS_BY_PACKAGE_NAME); @@ -114,6 +120,9 @@ public class KeychainExternalProvider extends ContentProvider implements SimpleC final int match = mUriMatcher.match(uri); switch (match) { case EMAIL_STATUS: + case EMAIL_STATUS_INTERNAL: + case AUTOCRYPT_PEER_STATUS: + case AUTOCRYPT_PEER_STATUS_INTERNAL: return EmailStatus.CONTENT_TYPE; case API_APPS: @@ -180,18 +189,10 @@ public class KeychainExternalProvider extends ContentProvider implements SimpleC + " WHEN " + Certs.VERIFIED_SECRET + " THEN " + KeychainExternalContract.KEY_STATUS_VERIFIED + " WHEN NULL THEN NULL" + " END AS " + EmailStatus.USER_ID_STATUS); - projectionMap.put(EmailStatus.AUTOCRYPT_PEER_STATE, "CASE ( MIN (certs_autocrypt_peer." + Certs.VERIFIED + " ) ) " - // remap to keep this provider contract independent from our internal representation - + " WHEN " + Certs.VERIFIED_SELF + " THEN " + KeychainExternalContract.KEY_STATUS_UNVERIFIED - + " WHEN " + Certs.VERIFIED_SECRET + " THEN " + KeychainExternalContract.KEY_STATUS_VERIFIED - + " WHEN NULL THEN NULL" - + " END AS " + EmailStatus.AUTOCRYPT_PEER_STATE); projectionMap.put(EmailStatus.MASTER_KEY_ID, Tables.USER_PACKETS + "." + UserPackets.MASTER_KEY_ID + " AS " + EmailStatus.MASTER_KEY_ID); projectionMap.put(EmailStatus.USER_ID, Tables.USER_PACKETS + "." + UserPackets.USER_ID + " AS " + EmailStatus.USER_ID); - projectionMap.put(EmailStatus.AUTOCRYPT_PEER_LAST_SEEN, Tables.API_AUTOCRYPT_PEERS + "." + - ApiAutocryptPeer.LAST_UPDATED + " AS " + EmailStatus.AUTOCRYPT_PEER_LAST_SEEN); qb.setProjectionMap(projectionMap); if (projection == null) { @@ -208,6 +209,66 @@ public class KeychainExternalProvider extends ContentProvider implements SimpleC + Tables.USER_PACKETS + "." + UserPackets.MASTER_KEY_ID + " = certs_user_id." + Certs.MASTER_KEY_ID + " AND " + Tables.USER_PACKETS + "." + UserPackets.RANK + " = certs_user_id." + Certs.RANK + ")" + ); + // in case there are multiple verifying certificates + groupBy = TEMP_TABLE_QUERIED_ADDRESSES + "." + TEMP_TABLE_COLUMN_ADDRES; + List plist = Arrays.asList(projection); + if (plist.contains(EmailStatus.USER_ID)) { + groupBy += ", " + Tables.USER_PACKETS + "." + UserPackets.USER_ID; + } + + if (TextUtils.isEmpty(sortOrder)) { + sortOrder = EmailStatus.EMAIL_ADDRESS; + } + + // uri to watch is all /key_rings/ + uri = KeyRings.CONTENT_URI; + break; + } + + case AUTOCRYPT_PEER_STATUS_INTERNAL: + if (!BuildConfig.APPLICATION_ID.equals(callingPackageName)) { + throw new AccessControlException("This URI can only be called internally!"); + } + + // override package name to use any external + // callingPackageName = uri.getLastPathSegment(); + + case AUTOCRYPT_PEER_STATUS: { + boolean callerIsAllowed = (match == AUTOCRYPT_PEER_STATUS_INTERNAL) || mApiPermissionHelper.isAllowedIgnoreErrors(); + if (!callerIsAllowed) { + throw new AccessControlException("An application must register before use of KeychainExternalProvider!"); + } + + db.execSQL("CREATE TEMPORARY TABLE " + TEMP_TABLE_QUERIED_ADDRESSES + " (" + TEMP_TABLE_COLUMN_ADDRES + " TEXT);"); + ContentValues cv = new ContentValues(); + for (String address : selectionArgs) { + cv.put(TEMP_TABLE_COLUMN_ADDRES, address); + db.insert(TEMP_TABLE_QUERIED_ADDRESSES, null, cv); + } + + HashMap projectionMap = new HashMap<>(); + projectionMap.put(AutocryptPeerStatus._ID, "email AS _id"); + projectionMap.put(AutocryptPeerStatus.EMAIL_ADDRESS, // this is actually the queried address + TEMP_TABLE_QUERIED_ADDRESSES + "." + TEMP_TABLE_COLUMN_ADDRES + " AS " + AutocryptPeerStatus.EMAIL_ADDRESS); + projectionMap.put(AutocryptPeerStatus.AUTOCRYPT_PEER_STATUS, "CASE ( MIN (certs_autocrypt_peer." + Certs.VERIFIED + " ) ) " + // remap to keep this provider contract independent from our internal representation + + " WHEN " + Certs.VERIFIED_SELF + " THEN " + KeychainExternalContract.KEY_STATUS_UNVERIFIED + + " WHEN " + Certs.VERIFIED_SECRET + " THEN " + KeychainExternalContract.KEY_STATUS_VERIFIED + + " WHEN NULL THEN NULL" + + " END AS " + AutocryptPeerStatus.AUTOCRYPT_PEER_STATUS); + projectionMap.put(AutocryptPeerStatus.MASTER_KEY_ID, + Tables.USER_PACKETS + "." + UserPackets.MASTER_KEY_ID + " AS " + AutocryptPeerStatus.MASTER_KEY_ID); + projectionMap.put(AutocryptPeerStatus.AUTOCRYPT_PEER_LAST_SEEN, Tables.API_AUTOCRYPT_PEERS + "." + + ApiAutocryptPeer.LAST_SEEN + " AS " + AutocryptPeerStatus.AUTOCRYPT_PEER_LAST_SEEN); + qb.setProjectionMap(projectionMap); + + if (projection == null) { + throw new IllegalArgumentException("Please provide a projection!"); + } + + qb.setTables( + TEMP_TABLE_QUERIED_ADDRESSES + " LEFT JOIN " + Tables.API_AUTOCRYPT_PEERS + " ON (" + Tables.API_AUTOCRYPT_PEERS + "." + ApiAutocryptPeer.IDENTIFIER + " LIKE queried_addresses.address" + " AND " + Tables.API_AUTOCRYPT_PEERS + "." + ApiAutocryptPeer.PACKAGE_NAME + " = \"" + callingPackageName + "\"" @@ -219,12 +280,9 @@ public class KeychainExternalProvider extends ContentProvider implements SimpleC // in case there are multiple verifying certificates groupBy = TEMP_TABLE_QUERIED_ADDRESSES + "." + TEMP_TABLE_COLUMN_ADDRES; List plist = Arrays.asList(projection); - if (plist.contains(EmailStatus.USER_ID)) { - groupBy += ", " + Tables.USER_PACKETS + "." + UserPackets.USER_ID; - } if (TextUtils.isEmpty(sortOrder)) { - sortOrder = EmailStatus.EMAIL_ADDRESS; + sortOrder = TEMP_TABLE_QUERIED_ADDRESSES + "." + TEMP_TABLE_COLUMN_ADDRES; } // uri to watch is all /key_rings/ @@ -246,7 +304,7 @@ public class KeychainExternalProvider extends ContentProvider implements SimpleC projectionMap.put(ApiAutocryptPeer._ID, "oid AS " + ApiAutocryptPeer._ID); projectionMap.put(ApiAutocryptPeer.IDENTIFIER, ApiAutocryptPeer.IDENTIFIER); projectionMap.put(ApiAutocryptPeer.MASTER_KEY_ID, ApiAutocryptPeer.MASTER_KEY_ID); - projectionMap.put(ApiAutocryptPeer.LAST_UPDATED, ApiAutocryptPeer.LAST_UPDATED); + projectionMap.put(ApiAutocryptPeer.LAST_SEEN, ApiAutocryptPeer.LAST_SEEN); qb.setProjectionMap(projectionMap); qb.setTables(Tables.API_AUTOCRYPT_PEERS); @@ -343,7 +401,7 @@ public class KeychainExternalProvider extends ContentProvider implements SimpleC actualValues.put(ApiAutocryptPeer.PACKAGE_NAME, mApiPermissionHelper.getCurrentCallingPackage()); actualValues.put(ApiAutocryptPeer.IDENTIFIER, uri.getLastPathSegment()); actualValues.put(ApiAutocryptPeer.MASTER_KEY_ID, masterKeyId); - actualValues.put(ApiAutocryptPeer.LAST_UPDATED, new Date().getTime() / 1000); + actualValues.put(ApiAutocryptPeer.LAST_SEEN, new Date().getTime() / 1000); SQLiteDatabase db = getDb().getWritableDatabase(); try { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java index 06157aabf..5a4de0038 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java @@ -41,10 +41,11 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.openintents.openpgp.AutocryptPeerUpdate; +import org.openintents.openpgp.AutocryptPeerUpdate.PreferEncrypt; import org.openintents.openpgp.IOpenPgpService; import org.openintents.openpgp.OpenPgpDecryptionResult; import org.openintents.openpgp.OpenPgpError; -import org.openintents.openpgp.AutocryptPeerUpdate; import org.openintents.openpgp.OpenPgpMetadata; import org.openintents.openpgp.OpenPgpSignatureResult; import org.openintents.openpgp.OpenPgpSignatureResult.AutocryptPeerResult; @@ -68,12 +69,12 @@ import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; import org.sufficientlysecure.keychain.provider.ApiDataAccessObject; +import org.sufficientlysecure.keychain.provider.AutocryptPeerDataAccessObject; import org.sufficientlysecure.keychain.provider.KeyRepository; import org.sufficientlysecure.keychain.provider.KeyWritableRepository; import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.OverriddenWarningsRepository; -import org.sufficientlysecure.keychain.provider.AutocryptPeerDataAccessObject; import org.sufficientlysecure.keychain.remote.OpenPgpServiceKeyIdExtractor.KeyIdResult; import org.sufficientlysecure.keychain.remote.OpenPgpServiceKeyIdExtractor.KeyIdResultStatus; import org.sufficientlysecure.keychain.service.BackupKeyringParcel; @@ -198,7 +199,7 @@ public class OpenPgpService extends Service { } private Intent encryptAndSignImpl(Intent data, InputStream inputStream, - OutputStream outputStream, boolean sign) { + OutputStream outputStream, boolean sign, boolean isQueryAutocryptStatus) { try { boolean asciiArmor = data.getBooleanExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); String originalFilename = data.getStringExtra(OpenPgpApi.EXTRA_ORIGINAL_FILENAME); @@ -237,11 +238,10 @@ public class OpenPgpService extends Service { KeyIdResult keyIdResult = mKeyIdExtractor.returnKeyIdsFromIntent(data, false, mApiPermissionHelper.getCurrentCallingPackage()); - boolean isDryRun = data.getBooleanExtra(OpenPgpApi.EXTRA_DRY_RUN, false); boolean isOpportunistic = data.getBooleanExtra(OpenPgpApi.EXTRA_OPPORTUNISTIC_ENCRYPTION, false); KeyIdResultStatus keyIdResultStatus = keyIdResult.getStatus(); - if (isDryRun) { - return getDryRunStatusResult(keyIdResult); + if (isQueryAutocryptStatus) { + return getAutocryptStatusResult(keyIdResult); } if (keyIdResult.hasKeySelectionPendingIntent()) { @@ -302,32 +302,34 @@ public class OpenPgpService extends Service { } @NonNull - private Intent getDryRunStatusResult(KeyIdResult keyIdResult) { + private Intent getAutocryptStatusResult(KeyIdResult keyIdResult) { + Intent result = new Intent(); + result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS); + switch (keyIdResult.getStatus()) { case MISSING: { - return createErrorResultIntent(OpenPgpError.OPPORTUNISTIC_MISSING_KEYS, - "missing keys in opportunistic mode"); + result.putExtra(OpenPgpApi.RESULT_AUTOCRYPT_STATUS, OpenPgpApi.AUTOCRYPT_STATUS_UNAVAILABLE); + break; } case NO_KEYS: case NO_KEYS_ERROR: { - return createErrorResultIntent(OpenPgpError.NO_USER_IDS, "empty recipient list"); + result.putExtra(OpenPgpApi.RESULT_AUTOCRYPT_STATUS, OpenPgpApi.AUTOCRYPT_STATUS_UNAVAILABLE); + break; } case DUPLICATE: { - Intent result = new Intent(); - result.putExtra(OpenPgpApi.RESULT_KEYS_CONFIRMED, false); - result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS); - return result; + result.putExtra(OpenPgpApi.RESULT_AUTOCRYPT_STATUS, OpenPgpApi.AUTOCRYPT_STATUS_DISCOURAGE); + break; } case OK: { - Intent result = new Intent(); - result.putExtra(OpenPgpApi.RESULT_KEYS_CONFIRMED, keyIdResult.isAllKeysConfirmed()); - result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS); - return result; + result.putExtra(OpenPgpApi.RESULT_AUTOCRYPT_STATUS, OpenPgpApi.AUTOCRYPT_STATUS_AVAILABLE); + break; } default: { throw new IllegalStateException("unhandled case!"); } } + + return result; } private Intent decryptAndVerifyImpl(Intent data, InputStream inputStream, OutputStream outputStream, @@ -452,35 +454,45 @@ public class OpenPgpService extends Service { private String updateAutocryptPeerStateFromIntent(Intent data, AutocryptPeerDataAccessObject autocryptPeerDao) throws PgpGeneralException, IOException { String autocryptPeerId = data.getStringExtra(OpenPgpApi.EXTRA_AUTOCRYPT_PEER_ID); - AutocryptPeerUpdate inlineKeyUpdate = data.getParcelableExtra(OpenPgpApi.EXTRA_INLINE_KEY_DATA); - if (inlineKeyUpdate == null) { + AutocryptPeerUpdate autocryptPeerUpdate = data.getParcelableExtra(OpenPgpApi.EXTRA_AUTOCRYPT_PEER_UPDATE); + if (autocryptPeerUpdate == null) { return null; } - UncachedKeyRing uncachedKeyRing = UncachedKeyRing.decodeFromData(inlineKeyUpdate.getKeyData()); - if (uncachedKeyRing.isSecret()) { - Log.e(Constants.TAG, "Found secret key in autocrypt id! - Ignoring"); - return null; - } - // this will merge if the key already exists - no worries! - KeyWritableRepository.createDatabaseReadWriteInteractor(this).savePublicKeyRing(uncachedKeyRing); - long inlineMasterKeyId = uncachedKeyRing.getMasterKeyId(); - - Date lastUpdate = autocryptPeerDao.getLastUpdateForAutocryptPeer(autocryptPeerId); - Date updateTimestamp = inlineKeyUpdate.getTimestamp(); - Long autocryptMasterKeyId = autocryptPeerDao.getMasterKeyIdForAutocryptPeer(autocryptPeerId); - - if (lastUpdate != null && lastUpdate.after(updateTimestamp)) { - Log.d(Constants.TAG, "Key for autocrypt peer is newer, ignoring other"); - return autocryptPeerId; - } else if (autocryptMasterKeyId == null) { - Log.d(Constants.TAG, "No binding for autocrypt peer, pinning key"); - autocryptPeerDao.setMasterKeyIdForAutocryptPeer(autocryptPeerId, inlineMasterKeyId, updateTimestamp); - } else if (inlineMasterKeyId == autocryptMasterKeyId) { - Log.d(Constants.TAG, "Key id is the same - doing nothing"); + Long newMasterKeyId; + if (autocryptPeerUpdate.hasKeyData()) { + UncachedKeyRing uncachedKeyRing = UncachedKeyRing.decodeFromData(autocryptPeerUpdate.getKeyData()); + if (uncachedKeyRing.isSecret()) { + Log.e(Constants.TAG, "Found secret key in autocrypt id! - Ignoring"); + return null; + } + // this will merge if the key already exists - no worries! + KeyWritableRepository.createDatabaseReadWriteInteractor(this).savePublicKeyRing(uncachedKeyRing); + newMasterKeyId = uncachedKeyRing.getMasterKeyId(); } else { - // TODO danger in result intent! - autocryptPeerDao.setMasterKeyIdForAutocryptPeer(autocryptPeerId, inlineMasterKeyId, updateTimestamp); + newMasterKeyId = null; + } + + Date lastSeen = autocryptPeerDao.getLastSeen(autocryptPeerId); + Date effectiveDate = autocryptPeerUpdate.getEffectiveDate(); + if (newMasterKeyId == null) { + if (effectiveDate.after(lastSeen)) { + autocryptPeerDao.updateToResetState(autocryptPeerId, effectiveDate); + } + return autocryptPeerId; + } + + Date lastSeenKey = autocryptPeerDao.getLastSeenKey(autocryptPeerId); + if (lastSeenKey != null && effectiveDate.before(lastSeenKey)) { + return autocryptPeerId; + } + + if (lastSeen == null || effectiveDate.after(lastSeen)) { + if (autocryptPeerUpdate.getPreferEncrypt() == PreferEncrypt.MUTUAL) { + autocryptPeerDao.updateToMutualState(autocryptPeerId, effectiveDate, newMasterKeyId); + } else { + autocryptPeerDao.updateToAvailableState(autocryptPeerId, effectiveDate, newMasterKeyId); + } } return autocryptPeerId; @@ -748,43 +760,17 @@ public class OpenPgpService extends Service { private Intent updateAutocryptPeerImpl(Intent data) { try { - Intent result = new Intent(); - - String autocryptPeer = data.getStringExtra(OpenPgpApi.EXTRA_AUTOCRYPT_PEER_ID); - AutocryptPeerUpdate inlineKeyUpdate = data.getParcelableExtra(OpenPgpApi.EXTRA_INLINE_KEY_DATA); - if (inlineKeyUpdate == null || autocryptPeer == null) { - throw new IllegalArgumentException("need to specify both autocrypt_peer_id and inline_key_data!"); + if (!data.hasExtra(OpenPgpApi.EXTRA_AUTOCRYPT_PEER_ID) || + !data.hasExtra(OpenPgpApi.EXTRA_AUTOCRYPT_PEER_UPDATE)) { + throw new IllegalArgumentException("need to specify both autocrypt_peer_id and autocrypt_peer_update!"); } - UncachedKeyRing uncachedKeyRing = UncachedKeyRing.decodeFromData(inlineKeyUpdate.getKeyData()); - long inlineMasterKeyId = uncachedKeyRing.getMasterKeyId(); - // this will merge if the key already exists - no worries! - KeyWritableRepository.createDatabaseReadWriteInteractor(this).savePublicKeyRing(uncachedKeyRing); - AutocryptPeerDataAccessObject autocryptPeerentityDao = new AutocryptPeerDataAccessObject(getBaseContext(), mApiPermissionHelper.getCurrentCallingPackage()); - Date lastUpdate = autocryptPeerentityDao.getLastUpdateForAutocryptPeer(autocryptPeer); - - Date updateTimestamp = inlineKeyUpdate.getTimestamp(); - boolean updateIsNewerThanLastUpdate = lastUpdate == null || lastUpdate.before(updateTimestamp); - if (!updateIsNewerThanLastUpdate) { - result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS); - return result; - } - Log.d(Constants.TAG, "Key for autocrypt peer is newer"); - - Long autocryptPeerMasterKeyId = autocryptPeerentityDao.getMasterKeyIdForAutocryptPeer(autocryptPeer); - if (autocryptPeerMasterKeyId == null) { - Log.d(Constants.TAG, "No binding for autocrypt peer, pinning key"); - autocryptPeerentityDao.setMasterKeyIdForAutocryptPeer(autocryptPeer, inlineMasterKeyId, updateTimestamp); - } else if (inlineMasterKeyId == autocryptPeerMasterKeyId) { - Log.d(Constants.TAG, "Key id is the same - doing nothing"); - } else { - // TODO danger in result intent! - autocryptPeerentityDao.setMasterKeyIdForAutocryptPeer(autocryptPeer, inlineMasterKeyId, updateTimestamp); - } + updateAutocryptPeerStateFromIntent(data, autocryptPeerentityDao); + Intent result = new Intent(); result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS); return result; } catch (Exception e) { @@ -940,11 +926,13 @@ public class OpenPgpService extends Service { case OpenPgpApi.ACTION_DETACHED_SIGN: { return signImpl(data, inputStream, outputStream, false); } - case OpenPgpApi.ACTION_ENCRYPT: { - return encryptAndSignImpl(data, inputStream, outputStream, false); + case OpenPgpApi.ACTION_QUERY_AUTOCRYPT_STATUS: { + return encryptAndSignImpl(data, inputStream, outputStream, false, true); } + case OpenPgpApi.ACTION_ENCRYPT: case OpenPgpApi.ACTION_SIGN_AND_ENCRYPT: { - return encryptAndSignImpl(data, inputStream, outputStream, true); + boolean enableSign = action.equals(OpenPgpApi.ACTION_SIGN_AND_ENCRYPT); + return encryptAndSignImpl(data, inputStream, outputStream, enableSign, false); } case OpenPgpApi.ACTION_DECRYPT_VERIFY: { return decryptAndVerifyImpl(data, inputStream, outputStream, false, progressable); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpServiceKeyIdExtractor.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpServiceKeyIdExtractor.java index db0e5e03b..5f88500ab 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpServiceKeyIdExtractor.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpServiceKeyIdExtractor.java @@ -17,6 +17,7 @@ import android.support.annotation.VisibleForTesting; import org.openintents.openpgp.util.OpenPgpApi; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.provider.KeychainExternalContract; +import org.sufficientlysecure.keychain.provider.KeychainExternalContract.AutocryptPeerStatus; import org.sufficientlysecure.keychain.provider.KeychainExternalContract.EmailStatus; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.util.Log; @@ -24,14 +25,23 @@ import org.sufficientlysecure.keychain.util.Log; class OpenPgpServiceKeyIdExtractor { @VisibleForTesting - static final String[] PROJECTION_KEY_SEARCH = { - "email_address", - "master_key_id", - "email_status", + static final String[] PROJECTION_MAIL_STATUS = { + EmailStatus.EMAIL_ADDRESS, + EmailStatus.MASTER_KEY_ID, + EmailStatus.USER_ID_STATUS, }; private static final int INDEX_EMAIL_ADDRESS = 0; private static final int INDEX_MASTER_KEY_ID = 1; - private static final int INDEX_EMAIL_STATUS = 2; + private static final int INDEX_USER_ID_STATUS = 2; + + static final String[] PROJECTION_AUTOCRYPT = { + AutocryptPeerStatus.EMAIL_ADDRESS, + AutocryptPeerStatus.MASTER_KEY_ID, + AutocryptPeerStatus.AUTOCRYPT_PEER_STATUS, + }; + private static final int INDEX_AUTOCRYPT_ADDRESS = 0; + private static final int INDEX_AUTOCRYPT_MASTER_KEY_ID = 1; + private static final int INDEX_AUTOCRYPT_STATUS = 2; private final ApiPendingIntentFactory apiPendingIntentFactory; @@ -88,6 +98,8 @@ class OpenPgpServiceKeyIdExtractor { if (hasAddresses) { HashMap keyRows = getStatusMapForQueriedAddresses(encryptionAddresses, callingPackageName); + HashMap autocryptRows = getAutocryptRecommendationsForQueriedAddresses( + encryptionAddresses, callingPackageName); boolean anyKeyNotVerified = false; for (Entry entry : keyRows.entrySet()) { @@ -105,7 +117,8 @@ class OpenPgpServiceKeyIdExtractor { duplicateEmails.add(queriedAddress); } - if (!userIdStatus.verified) { + if (userIdStatus.userIdVerified != KeychainExternalContract.KEY_STATUS_VERIFIED && + userIdStatus.autocryptPeerVerified != KeychainExternalContract.KEY_STATUS_VERIFIED) { anyKeyNotVerified = true; } } @@ -141,7 +154,7 @@ class OpenPgpServiceKeyIdExtractor { private HashMap getStatusMapForQueriedAddresses(String[] encryptionUserIds, String callingPackageName) { HashMap keyRows = new HashMap<>(); Uri queryUri = EmailStatus.CONTENT_URI.buildUpon().appendPath(callingPackageName).build(); - Cursor cursor = contentResolver.query(queryUri, PROJECTION_KEY_SEARCH, null, encryptionUserIds, null); + Cursor cursor = contentResolver.query(queryUri, PROJECTION_MAIL_STATUS, null, encryptionUserIds, null); if (cursor == null) { throw new IllegalStateException("Internal error, received null cursor!"); } @@ -150,21 +163,33 @@ class OpenPgpServiceKeyIdExtractor { while (cursor.moveToNext()) { String queryAddress = cursor.getString(INDEX_EMAIL_ADDRESS); Long masterKeyId = cursor.isNull(INDEX_MASTER_KEY_ID) ? null : cursor.getLong(INDEX_MASTER_KEY_ID); - boolean isVerified = cursor.getInt(INDEX_EMAIL_STATUS) == KeychainExternalContract.KEY_STATUS_VERIFIED; - UserIdStatus userIdStatus = new UserIdStatus(masterKeyId, isVerified); + int userIdStatus = cursor.getInt(INDEX_USER_ID_STATUS); + + AutocryptRecommendation autocryptRecommendation; + if (cursor.getInt(INDEX_AUTOCRYPT_PEER_STATUS) == KeychainExternalContract.KEY_STATUS_UNAVAILABLE) { + autocryptRecommendation = AutocryptRecommendation.UNAVAILABLE; + } else { + // TODO encourage/discourage, based on gossip/ state + autocryptRecommendation = AutocryptRecommendation.AVAILABLE; + } + UserIdStatus status = new UserIdStatus(masterKeyId, userIdStatus, autocryptRecommendation); boolean seenBefore = keyRows.containsKey(queryAddress); if (!seenBefore) { - keyRows.put(queryAddress, userIdStatus); + keyRows.put(queryAddress, status); continue; } UserIdStatus previousUserIdStatus = keyRows.get(queryAddress); + if (previousUserIdStatus.autocryptPeerVerified != KeychainExternalContract.KEY_STATUS_UNAVAILABLE) { + continue; + } + if (previousUserIdStatus.masterKeyId == null) { - keyRows.put(queryAddress, userIdStatus); - } else if (!previousUserIdStatus.verified && userIdStatus.verified) { - keyRows.put(queryAddress, userIdStatus); - } else if (previousUserIdStatus.verified == userIdStatus.verified) { + keyRows.put(queryAddress, status); + } else if (previousUserIdStatus.userIdVerified < status.userIdVerified) { + keyRows.put(queryAddress, status); + } else if (previousUserIdStatus.userIdVerified == status.userIdVerified) { previousUserIdStatus.hasDuplicate = true; } } @@ -174,14 +199,50 @@ class OpenPgpServiceKeyIdExtractor { return keyRows; } + /** This method queries the KeychainExternalProvider for all addresses given in encryptionUserIds. + * It returns a map with one UserIdStatus per queried address. If multiple key candidates exist, + * the one with the highest verification status is selected. If two candidates with the same + * verification status exist, the first one is returned and marked as having a duplicate. + */ + @NonNull + private HashMap getAutocryptRecommendationsForQueriedAddresses( + String[] encryptionUserIds, String callingPackageName) { + Uri queryUri = AutocryptPeerStatus.CONTENT_URI.buildUpon().appendPath(callingPackageName).build(); + Cursor cursor = contentResolver.query(queryUri, PROJECTION_AUTOCRYPT, null, encryptionUserIds, null); + if (cursor == null) { + throw new IllegalStateException("Internal error, received null cursor!"); + } + + try { + HashMap keyRows = new HashMap<>(); + while (cursor.moveToNext()) { + String queryAddress = cursor.getString(INDEX_AUTOCRYPT_ADDRESS); + Long masterKeyId = cursor.isNull(INDEX_AUTOCRYPT_MASTER_KEY_ID) ? + null : cursor.getLong(INDEX_AUTOCRYPT_MASTER_KEY_ID); + int autocryptStatus = cursor.getInt(INDEX_AUTOCRYPT_STATUS); + + AutocryptRecommendation autocryptRecommendation; + if (cursor.getInt(INDEX_AUTOCRYPT_STATUS) == KeychainExternalContract.KEY_STATUS_UNAVAILABLE) { + autocryptRecommendation = AutocryptRecommendation.UNAVAILABLE; + } else { + // TODO encourage/discourage, based on gossip/ state + autocryptRecommendation = AutocryptRecommendation.AVAILABLE; + } + } + } finally { + cursor.close(); + } + return keyRows; + } + private static class UserIdStatus { private final Long masterKeyId; - private final boolean verified; + private final int userIdVerified; private boolean hasDuplicate; - UserIdStatus(Long masterKeyId, boolean verified) { + UserIdStatus(Long masterKeyId, int userIdVerified) { this.masterKeyId = masterKeyId; - this.verified = verified; + this.userIdVerified = userIdVerified; } } @@ -256,6 +317,10 @@ class OpenPgpServiceKeyIdExtractor { } } + enum AutocryptRecommendation { + UNAVAILABLE, DISCOURAGE, AVAILABLE, MUTUAL + } + enum KeyIdResultStatus { OK, MISSING, DUPLICATE, NO_KEYS, NO_KEYS_ERROR } diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/remote/KeychainExternalProviderTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/remote/KeychainExternalProviderTest.java index c6e05d7b6..cfa192bb7 100644 --- a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/remote/KeychainExternalProviderTest.java +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/remote/KeychainExternalProviderTest.java @@ -187,7 +187,7 @@ public class KeychainExternalProviderTest { Cursor cursor = contentResolver.query( EmailStatus.CONTENT_URI, new String[] { EmailStatus.EMAIL_ADDRESS, EmailStatus.USER_ID_STATUS, EmailStatus.USER_ID, - EmailStatus.AUTOCRYPT_PEER_STATE }, + EmailStatus.AUTOCRYPT_PEER_STATUS }, null, new String [] { AUTOCRYPT_PEER }, null ); @@ -211,7 +211,7 @@ public class KeychainExternalProviderTest { Cursor cursor = contentResolver.query( EmailStatus.CONTENT_URI, new String[] { EmailStatus.EMAIL_ADDRESS, EmailStatus.USER_ID_STATUS, EmailStatus.USER_ID, - EmailStatus.AUTOCRYPT_PEER_STATE }, + EmailStatus.AUTOCRYPT_PEER_STATUS }, null, new String [] { AUTOCRYPT_PEER }, null ); diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/remote/OpenPgpServiceKeyIdExtractorTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/remote/OpenPgpServiceKeyIdExtractorTest.java index 2cf52a05a..c50a04278 100644 --- a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/remote/OpenPgpServiceKeyIdExtractorTest.java +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/remote/OpenPgpServiceKeyIdExtractorTest.java @@ -205,14 +205,14 @@ public class OpenPgpServiceKeyIdExtractorTest { } private void setupContentResolverResult() { - MatrixCursor resultCursor = new MatrixCursor(OpenPgpServiceKeyIdExtractor.PROJECTION_KEY_SEARCH); + MatrixCursor resultCursor = new MatrixCursor(OpenPgpServiceKeyIdExtractor.PROJECTION_MAIL_STATUS); when(contentResolver.query( any(Uri.class), any(String[].class), any(String.class), any(String[].class), any(String.class))) .thenReturn(resultCursor); } private void setupContentResolverResult(String[] userIds, Long[] resultKeyIds, int[] verified) { - MatrixCursor resultCursor = new MatrixCursor(OpenPgpServiceKeyIdExtractor.PROJECTION_KEY_SEARCH); + MatrixCursor resultCursor = new MatrixCursor(OpenPgpServiceKeyIdExtractor.PROJECTION_MAIL_STATUS); for (int i = 0; i < userIds.length; i++) { resultCursor.addRow(new Object[] { userIds[i], resultKeyIds[i], verified[i] }); } diff --git a/extern/openpgp-api-lib b/extern/openpgp-api-lib index fde49c854..6d81f4700 160000 --- a/extern/openpgp-api-lib +++ b/extern/openpgp-api-lib @@ -1 +1 @@ -Subproject commit fde49c854fd7aa217169061b35b97c9231119c18 +Subproject commit 6d81f4700e5f8da6226582fc2f94d6cf8f1f0a75