package org.sufficientlysecure.keychain.remote; import java.io.IOException; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Map; import android.content.Context; import androidx.annotation.Nullable; import android.text.format.DateUtils; import org.openintents.openpgp.AutocryptPeerUpdate; import org.openintents.openpgp.AutocryptPeerUpdate.PreferEncrypt; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.model.AutocryptPeer; import org.sufficientlysecure.keychain.model.AutocryptPeer.AutocryptKeyStatus; import org.sufficientlysecure.keychain.model.AutocryptPeer.GossipOrigin; import org.sufficientlysecure.keychain.operations.results.SaveKeyringResult; import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; import org.sufficientlysecure.keychain.daos.AutocryptPeerDao; import org.sufficientlysecure.keychain.daos.KeyWritableRepository; import timber.log.Timber; public class AutocryptInteractor { private static final long AUTOCRYPT_DISCOURAGE_THRESHOLD_MILLIS = 35 * DateUtils.DAY_IN_MILLIS; private AutocryptPeerDao autocryptPeerDao; private KeyWritableRepository keyWritableRepository; private final String packageName; public static AutocryptInteractor getInstance(Context context, String packageName) { AutocryptPeerDao autocryptPeerDao = AutocryptPeerDao.getInstance(context); KeyWritableRepository keyWritableRepository = KeyWritableRepository.create(context); return new AutocryptInteractor(autocryptPeerDao, keyWritableRepository, packageName); } private AutocryptInteractor(AutocryptPeerDao autocryptPeerDao, KeyWritableRepository keyWritableRepository, String packageName) { this.autocryptPeerDao = autocryptPeerDao; this.keyWritableRepository = keyWritableRepository; this.packageName = packageName; } void updateAutocryptPeerState(String autocryptPeerId, AutocryptPeerUpdate autocryptPeerUpdate) { AutocryptPeer currentAutocryptPeer = autocryptPeerDao.getAutocryptPeer(packageName, autocryptPeerId); Date effectiveDate = autocryptPeerUpdate.getEffectiveDate(); // 1. If the message’s effective date is older than the peers[from-addr].autocrypt_timestamp value, then no changes are required, and the update process terminates. Date lastSeenKey = currentAutocryptPeer != null ? currentAutocryptPeer.last_seen_key() : null; if (lastSeenKey != null && effectiveDate.compareTo(lastSeenKey) <= 0) { return; } // 2. If the message’s effective date is more recent than peers[from-addr].last_seen then set peers[from-addr].last_seen to the message’s effective date. Date lastSeen = currentAutocryptPeer != null ? currentAutocryptPeer.last_seen() : null; if (lastSeen == null || effectiveDate.after(lastSeen)) { autocryptPeerDao.insertOrUpdateLastSeen(packageName, autocryptPeerId, effectiveDate); } // 3. If the Autocrypt header is unavailable, no further changes are required and the update process terminates. if (!autocryptPeerUpdate.hasKeyData()) { return; } SaveKeyringResult saveKeyringResult = parseAndImportAutocryptKeyData(autocryptPeerUpdate); if (saveKeyringResult == null) { return; } // 4. Set peers[from-addr].autocrypt_timestamp to the message’s effective date. // 5. Set peers[from-addr].public_key to the corresponding keydata value of the Autocrypt header. Long newMasterKeyId = saveKeyringResult.savedMasterKeyId; // 6. Set peers[from-addr].prefer_encrypt to the corresponding prefer-encrypt value of the Autocrypt header. boolean isMutual = autocryptPeerUpdate.getPreferEncrypt() == PreferEncrypt.MUTUAL; autocryptPeerDao.updateKey(packageName, autocryptPeerId, effectiveDate, newMasterKeyId, isMutual); } void updateAutocryptPeerGossipState(String autocryptPeerId, AutocryptPeerUpdate autocryptPeerUpdate) { AutocryptPeer currentAutocryptPeer = autocryptPeerDao.getAutocryptPeer(packageName, autocryptPeerId); Date effectiveDate = autocryptPeerUpdate.getEffectiveDate(); // 1. If gossip-addr does not match any recipient in the mail’s To or Cc header, the update process terminates (i.e., header is ignored). // -> This should be taken care of in the mail client that sends us this data! // 2. If peers[gossip-addr].gossip_timestamp is more recent than the message’s effective date, then the update process terminates. Date lastSeenGossip = currentAutocryptPeer != null ? currentAutocryptPeer.gossip_last_seen_key() : null; if (lastSeenGossip != null && lastSeenGossip.after(effectiveDate)) { return; } if (!autocryptPeerUpdate.hasKeyData()) { return; } SaveKeyringResult saveKeyringResult = parseAndImportAutocryptKeyData(autocryptPeerUpdate); if (saveKeyringResult == null) { return; } // 3. Set peers[gossip-addr].gossip_timestamp to the message’s effective date. // 4. Set peers[gossip-addr].gossip_key to the value of the keydata attribute. Long newMasterKeyId = saveKeyringResult.savedMasterKeyId; autocryptPeerDao.updateKeyGossip(packageName, autocryptPeerId, effectiveDate, newMasterKeyId, GossipOrigin.GOSSIP_HEADER); } @Nullable private SaveKeyringResult parseAndImportAutocryptKeyData(AutocryptPeerUpdate autocryptPeerUpdate) { UncachedKeyRing uncachedKeyRing = parseAutocryptKeyData(autocryptPeerUpdate); if (uncachedKeyRing != null) { return importAutocryptKeyData(uncachedKeyRing); } return null; } @Nullable private SaveKeyringResult importAutocryptKeyData(UncachedKeyRing uncachedKeyRing) { SaveKeyringResult saveKeyringResult = keyWritableRepository.savePublicKeyRing(uncachedKeyRing); if (!saveKeyringResult.success()) { Timber.e(Constants.TAG, "Error inserting key - ignoring!"); return null; } return saveKeyringResult; } @Nullable private UncachedKeyRing parseAutocryptKeyData(AutocryptPeerUpdate autocryptPeerUpdate) { UncachedKeyRing uncachedKeyRing; try { uncachedKeyRing = UncachedKeyRing.decodeFromData(autocryptPeerUpdate.getKeyData()); } catch (IOException | PgpGeneralException e) { Timber.e(Constants.TAG, "Error parsing public key! - Ignoring"); return null; } if (uncachedKeyRing.isSecret()) { Timber.e(Constants.TAG, "Found secret key in autocrypt id! - Ignoring"); return null; } return uncachedKeyRing; } public Map determineAutocryptRecommendations(String... autocryptIds) { Map result = new HashMap<>(autocryptIds.length); for (AutocryptKeyStatus autocryptKeyStatus : autocryptPeerDao.getAutocryptKeyStatus(packageName, autocryptIds)) { AutocryptRecommendationResult peerResult = determineAutocryptRecommendation(autocryptKeyStatus); result.put(peerResult.peerId, peerResult); } return Collections.unmodifiableMap(result); } /** Determines Autocrypt "ui-recommendation", according to spec. * See https://autocrypt.org/level1.html#recommendations-for-single-recipient-messages */ private AutocryptRecommendationResult determineAutocryptRecommendation(AutocryptKeyStatus autocryptKeyStatus) { AutocryptRecommendationResult keyRecommendation = determineAutocryptKeyRecommendation(autocryptKeyStatus); if (keyRecommendation != null) return keyRecommendation; AutocryptRecommendationResult gossipRecommendation = determineAutocryptGossipRecommendation(autocryptKeyStatus); if (gossipRecommendation != null) return gossipRecommendation; return new AutocryptRecommendationResult(autocryptKeyStatus.autocryptPeer().identifier(), AutocryptState.DISABLE, null, false); } @Nullable private AutocryptRecommendationResult determineAutocryptKeyRecommendation(AutocryptKeyStatus autocryptKeyStatus) { AutocryptPeer autocryptPeer = autocryptKeyStatus.autocryptPeer(); Long masterKeyId = autocryptPeer.master_key_id(); boolean hasKey = masterKeyId != null; boolean isRevoked = autocryptKeyStatus.isKeyRevoked(); boolean isExpired = autocryptKeyStatus.isKeyExpired(); if (!hasKey || isRevoked || isExpired) { return null; } Date lastSeen = autocryptPeer.last_seen(); Date lastSeenKey = autocryptPeer.last_seen_key(); boolean isVerified = autocryptKeyStatus.isKeyVerified(); boolean isLastSeenOlderThanDiscourageTimespan = lastSeen != null && lastSeenKey != null && lastSeenKey.getTime() < (lastSeen.getTime() - AUTOCRYPT_DISCOURAGE_THRESHOLD_MILLIS); if (isLastSeenOlderThanDiscourageTimespan) { return new AutocryptRecommendationResult(autocryptPeer.identifier(), AutocryptState.DISCOURAGED_OLD, masterKeyId, isVerified); } boolean isMutual = autocryptPeer.is_mutual(); if (isMutual) { return new AutocryptRecommendationResult(autocryptPeer.identifier(), AutocryptState.MUTUAL, masterKeyId, isVerified); } else { return new AutocryptRecommendationResult(autocryptPeer.identifier(), AutocryptState.AVAILABLE, masterKeyId, isVerified); } } @Nullable private AutocryptRecommendationResult determineAutocryptGossipRecommendation(AutocryptKeyStatus autocryptKeyStatus) { boolean gossipHasKey = autocryptKeyStatus.hasGossipKey(); boolean gossipIsRevoked = autocryptKeyStatus.isGossipKeyRevoked(); boolean gossipIsExpired = autocryptKeyStatus.isGossipKeyExpired(); boolean isVerified = autocryptKeyStatus.isGossipKeyVerified(); if (!gossipHasKey || gossipIsRevoked || gossipIsExpired) { return null; } Long masterKeyId = autocryptKeyStatus.autocryptPeer().gossip_master_key_id(); return new AutocryptRecommendationResult(autocryptKeyStatus.autocryptPeer().identifier(), AutocryptState.DISCOURAGED_GOSSIP, masterKeyId, isVerified); } public void updateKeyGossipFromSignature(String autocryptId, Date effectiveDate, long masterKeyId) { autocryptPeerDao.updateKeyGossip(packageName, autocryptId, effectiveDate, masterKeyId, GossipOrigin.SIGNATURE); } public void updateKeyGossipFromDedup(String autocryptId, long masterKeyId) { autocryptPeerDao.updateKeyGossip(packageName, autocryptId, new Date(), masterKeyId, GossipOrigin.DEDUP); } public static class AutocryptRecommendationResult { public final String peerId; public final Long masterKeyId; public final AutocryptState autocryptState; public final boolean isVerified; AutocryptRecommendationResult(String peerId, AutocryptState autocryptState, Long masterKeyId, boolean isVerified) { this.peerId = peerId; this.autocryptState = autocryptState; this.masterKeyId = masterKeyId; this.isVerified = isVerified; } } public enum AutocryptState { DISABLE, DISCOURAGED_OLD, DISCOURAGED_GOSSIP, AVAILABLE, MUTUAL } }