246 lines
11 KiB
Java
246 lines
11 KiB
Java
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<String,AutocryptRecommendationResult> determineAutocryptRecommendations(String... autocryptIds) {
|
||
Map<String,AutocryptRecommendationResult> 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
|
||
}
|
||
}
|