From 4ead7cd4a1604ba4cf9e9c8e611f3af02af23048 Mon Sep 17 00:00:00 2001 From: akwizgran Date: Tue, 23 Feb 2021 17:22:56 +0000 Subject: [PATCH] WIP: Update our backup when contacts are added or removed. --- .../socialbackup/BackupPayloadEncoder.java | 8 +- .../BackupPayloadEncoderImpl.java | 57 +++--- .../briar/socialbackup/ContactData.java | 43 ++++ .../briar/socialbackup/MessageParser.java | 5 +- .../briar/socialbackup/MessageParserImpl.java | 22 +- .../socialbackup/SocialBackupConstants.java | 1 + .../socialbackup/SocialBackupManagerImpl.java | 193 +++++++++++++++--- 7 files changed, 246 insertions(+), 83 deletions(-) create mode 100644 briar-core/src/main/java/org/briarproject/briar/socialbackup/ContactData.java diff --git a/briar-core/src/main/java/org/briarproject/briar/socialbackup/BackupPayloadEncoder.java b/briar-core/src/main/java/org/briarproject/briar/socialbackup/BackupPayloadEncoder.java index 2c3abc411..45341fdc1 100644 --- a/briar-core/src/main/java/org/briarproject/briar/socialbackup/BackupPayloadEncoder.java +++ b/briar-core/src/main/java/org/briarproject/briar/socialbackup/BackupPayloadEncoder.java @@ -1,20 +1,14 @@ package org.briarproject.briar.socialbackup; -import org.briarproject.bramble.api.contact.Contact; import org.briarproject.bramble.api.crypto.SecretKey; import org.briarproject.bramble.api.identity.Identity; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; -import org.briarproject.bramble.api.plugin.TransportId; -import org.briarproject.bramble.api.properties.TransportProperties; import java.util.List; -import java.util.Map; @NotNullByDefault interface BackupPayloadEncoder { BackupPayload encodeBackupPayload(SecretKey secret, Identity identity, - List contacts, - List> properties, - int version); + List contactData, int version); } diff --git a/briar-core/src/main/java/org/briarproject/briar/socialbackup/BackupPayloadEncoderImpl.java b/briar-core/src/main/java/org/briarproject/briar/socialbackup/BackupPayloadEncoderImpl.java index 3e796b3dd..c7953afc8 100644 --- a/briar-core/src/main/java/org/briarproject/briar/socialbackup/BackupPayloadEncoderImpl.java +++ b/briar-core/src/main/java/org/briarproject/briar/socialbackup/BackupPayloadEncoderImpl.java @@ -10,19 +10,16 @@ import org.briarproject.bramble.api.data.BdfList; import org.briarproject.bramble.api.identity.Identity; import org.briarproject.bramble.api.identity.LocalAuthor; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; -import org.briarproject.bramble.api.plugin.TransportId; -import org.briarproject.bramble.api.properties.TransportProperties; +import org.briarproject.briar.api.socialbackup.Shard; import java.security.GeneralSecurityException; import java.security.SecureRandom; import java.util.List; -import java.util.Map; import javax.annotation.concurrent.Immutable; import javax.inject.Inject; import javax.inject.Provider; -import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull; import static org.briarproject.briar.socialbackup.SocialBackupConstants.AUTH_TAG_BYTES; import static org.briarproject.briar.socialbackup.SocialBackupConstants.NONCE_BYTES; @@ -33,49 +30,49 @@ class BackupPayloadEncoderImpl implements BackupPayloadEncoder { private final ClientHelper clientHelper; private final Provider cipherProvider; private final SecureRandom secureRandom; + private final MessageEncoder messageEncoder; @Inject BackupPayloadEncoderImpl(ClientHelper clientHelper, Provider cipherProvider, - SecureRandom secureRandom) { + SecureRandom secureRandom, + MessageEncoder messageEncoder) { this.clientHelper = clientHelper; this.cipherProvider = cipherProvider; this.secureRandom = secureRandom; + this.messageEncoder = messageEncoder; } @Override public BackupPayload encodeBackupPayload(SecretKey secret, - Identity identity, List contacts, - List> properties, - int version) { - if (contacts.size() != properties.size()) { - throw new IllegalArgumentException(); - } + Identity identity, List contactData, int version) { // Encode the local identity - BdfList identityData = new BdfList(); + BdfList bdfIdentity = new BdfList(); LocalAuthor localAuthor = identity.getLocalAuthor(); - identityData.add(clientHelper.toList(localAuthor)); - identityData.add(localAuthor.getPrivateKey().getEncoded()); - identityData.add(identity.getHandshakePublicKey().getEncoded()); - identityData.add(identity.getHandshakePrivateKey().getEncoded()); - // Encode the contacts - BdfList contactData = new BdfList(); - for (int i = 0; i < contacts.size(); i++) { - Contact contact = contacts.get(i); - Map props = properties.get(i); - BdfList data = new BdfList(); - data.add(clientHelper.toList(contact.getAuthor())); - data.add(contact.getAlias()); - PublicKey pub = requireNonNull(contact.getHandshakePublicKey()); - data.add(pub.getEncoded()); - data.add(clientHelper.toDictionary(props)); - contactData.add(data); + bdfIdentity.add(clientHelper.toList(localAuthor)); + bdfIdentity.add(localAuthor.getPrivateKey().getEncoded()); + bdfIdentity.add(identity.getHandshakePublicKey().getEncoded()); + bdfIdentity.add(identity.getHandshakePrivateKey().getEncoded()); + // Encode the contact data + BdfList bdfContactData = new BdfList(); + for (ContactData cd : contactData) { + BdfList bdfData = new BdfList(); + Contact contact = cd.getContact(); + bdfData.add(clientHelper.toList(contact.getAuthor())); + bdfData.add(contact.getAlias()); + PublicKey pub = contact.getHandshakePublicKey(); + bdfData.add(pub == null ? null : pub.getEncoded()); + bdfData.add(clientHelper.toDictionary(cd.getProperties())); + Shard shard = cd.getShard(); + if (shard == null) bdfData.add(null); + else bdfData.add(messageEncoder.encodeShardMessage(shard)); + bdfContactData.add(bdfData); } // Encode and encrypt the payload BdfList backup = new BdfList(); backup.add(version); - backup.add(identityData); - backup.add(contactData); + backup.add(bdfIdentity); + backup.add(bdfContactData); try { byte[] plaintext = clientHelper.toByteArray(backup); byte[] ciphertext = new byte[plaintext.length + AUTH_TAG_BYTES]; diff --git a/briar-core/src/main/java/org/briarproject/briar/socialbackup/ContactData.java b/briar-core/src/main/java/org/briarproject/briar/socialbackup/ContactData.java new file mode 100644 index 000000000..10778858a --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/socialbackup/ContactData.java @@ -0,0 +1,43 @@ +package org.briarproject.briar.socialbackup; + +import org.briarproject.bramble.api.contact.Contact; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.TransportId; +import org.briarproject.bramble.api.properties.TransportProperties; +import org.briarproject.briar.api.socialbackup.Shard; + +import java.util.Map; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +@Immutable +@NotNullByDefault +class ContactData { + + private final Contact contact; + private final Map properties; + @Nullable + private final Shard shard; + + ContactData(Contact contact, + Map properties, + @Nullable Shard shard) { + this.contact = contact; + this.properties = properties; + this.shard = shard; + } + + public Contact getContact() { + return contact; + } + + public Map getProperties() { + return properties; + } + + @Nullable + public Shard getShard() { + return shard; + } +} diff --git a/briar-core/src/main/java/org/briarproject/briar/socialbackup/MessageParser.java b/briar-core/src/main/java/org/briarproject/briar/socialbackup/MessageParser.java index 216e1bbf8..36dbe6ea9 100644 --- a/briar-core/src/main/java/org/briarproject/briar/socialbackup/MessageParser.java +++ b/briar-core/src/main/java/org/briarproject/briar/socialbackup/MessageParser.java @@ -1,13 +1,14 @@ package org.briarproject.briar.socialbackup; import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.data.BdfList; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.briar.api.socialbackup.Shard; @NotNullByDefault interface MessageParser { - Shard parseShardMessage(byte[] body) throws FormatException; + Shard parseShardMessage(BdfList body) throws FormatException; - BackupPayload parseBackupMessage(byte[] body) throws FormatException; + BackupPayload parseBackupMessage(BdfList body) throws FormatException; } diff --git a/briar-core/src/main/java/org/briarproject/briar/socialbackup/MessageParserImpl.java b/briar-core/src/main/java/org/briarproject/briar/socialbackup/MessageParserImpl.java index 3a769e732..076346966 100644 --- a/briar-core/src/main/java/org/briarproject/briar/socialbackup/MessageParserImpl.java +++ b/briar-core/src/main/java/org/briarproject/briar/socialbackup/MessageParserImpl.java @@ -1,7 +1,6 @@ package org.briarproject.briar.socialbackup; import org.briarproject.bramble.api.FormatException; -import org.briarproject.bramble.api.client.ClientHelper; import org.briarproject.bramble.api.data.BdfList; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.briar.api.socialbackup.Shard; @@ -13,29 +12,24 @@ import javax.inject.Inject; @NotNullByDefault class MessageParserImpl implements MessageParser { - private final ClientHelper clientHelper; - @Inject - MessageParserImpl(ClientHelper clientHelper) { - this.clientHelper = clientHelper; + MessageParserImpl() { } @Override - public Shard parseShardMessage(byte[] body) throws FormatException { - BdfList list = clientHelper.toList(body); + public Shard parseShardMessage(BdfList body) throws FormatException { // Message type, secret ID, num shards, threshold, shard - byte[] secretId = list.getRaw(1); - int numShards = list.getLong(2).intValue(); - int threshold = list.getLong(3).intValue(); - byte[] shard = list.getRaw(4); + byte[] secretId = body.getRaw(1); + int numShards = body.getLong(2).intValue(); + int threshold = body.getLong(3).intValue(); + byte[] shard = body.getRaw(4); return new Shard(secretId, numShards, threshold, shard); } @Override - public BackupPayload parseBackupMessage(byte[] body) + public BackupPayload parseBackupMessage(BdfList body) throws FormatException { - BdfList list = clientHelper.toList(body); // Message type, backup payload - return new BackupPayload(list.getRaw(1)); + return new BackupPayload(body.getRaw(1)); } } diff --git a/briar-core/src/main/java/org/briarproject/briar/socialbackup/SocialBackupConstants.java b/briar-core/src/main/java/org/briarproject/briar/socialbackup/SocialBackupConstants.java index 2f40b8090..924609a72 100644 --- a/briar-core/src/main/java/org/briarproject/briar/socialbackup/SocialBackupConstants.java +++ b/briar-core/src/main/java/org/briarproject/briar/socialbackup/SocialBackupConstants.java @@ -3,6 +3,7 @@ package org.briarproject.briar.socialbackup; interface SocialBackupConstants { // Group metadata keys + String GROUP_KEY_CONTACT_ID = "contactId"; String GROUP_KEY_SECRET = "secret"; String GROUP_KEY_CUSTODIANS = "custodians"; String GROUP_KEY_THRESHOLD = "threshold"; diff --git a/briar-core/src/main/java/org/briarproject/briar/socialbackup/SocialBackupManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/socialbackup/SocialBackupManagerImpl.java index e1ec098a3..f864d9427 100644 --- a/briar-core/src/main/java/org/briarproject/briar/socialbackup/SocialBackupManagerImpl.java +++ b/briar-core/src/main/java/org/briarproject/briar/socialbackup/SocialBackupManagerImpl.java @@ -1,6 +1,7 @@ package org.briarproject.briar.socialbackup; import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.Pair; import org.briarproject.bramble.api.client.BdfIncomingMessageHook; import org.briarproject.bramble.api.client.ClientHelper; import org.briarproject.bramble.api.client.ContactGroupFactory; @@ -16,10 +17,12 @@ import org.briarproject.bramble.api.data.BdfList; import org.briarproject.bramble.api.data.MetadataParser; import org.briarproject.bramble.api.db.DatabaseComponent; import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.db.NoSuchContactException; import org.briarproject.bramble.api.db.Transaction; import org.briarproject.bramble.api.identity.Author; import org.briarproject.bramble.api.identity.Identity; import org.briarproject.bramble.api.identity.IdentityManager; +import org.briarproject.bramble.api.identity.LocalAuthor; import org.briarproject.bramble.api.lifecycle.LifecycleManager.OpenDatabaseHook; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.plugin.TorConstants; @@ -42,6 +45,7 @@ import org.briarproject.briar.api.socialbackup.SocialBackupManager; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.ListIterator; import java.util.Map; import java.util.Map.Entry; @@ -49,8 +53,11 @@ import javax.annotation.Nullable; import javax.inject.Inject; import static java.util.Collections.singletonMap; +import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull; import static org.briarproject.briar.socialbackup.MessageType.BACKUP; import static org.briarproject.briar.socialbackup.MessageType.SHARD; +import static org.briarproject.briar.socialbackup.SocialBackupConstants.GROUP_KEY_CONTACT_ID; +import static org.briarproject.briar.socialbackup.SocialBackupConstants.GROUP_KEY_VERSION; import static org.briarproject.briar.socialbackup.SocialBackupConstants.MSG_KEY_LOCAL; import static org.briarproject.briar.socialbackup.SocialBackupConstants.MSG_KEY_MESSAGE_TYPE; import static org.briarproject.briar.socialbackup.SocialBackupConstants.MSG_KEY_VERSION; @@ -66,6 +73,7 @@ class SocialBackupManagerImpl extends BdfIncomingMessageHook private final BackupMetadataParser backupMetadataParser; private final BackupMetadataEncoder backupMetadataEncoder; private final BackupPayloadEncoder backupPayloadEncoder; + private final MessageParser messageParser; private final MessageEncoder messageEncoder; private final IdentityManager identityManager; private final ContactManager contactManager; @@ -85,6 +93,7 @@ class SocialBackupManagerImpl extends BdfIncomingMessageHook BackupMetadataParser backupMetadataParser, BackupMetadataEncoder backupMetadataEncoder, BackupPayloadEncoder backupPayloadEncoder, + MessageParser messageParser, MessageEncoder messageEncoder, IdentityManager identityManager, ContactManager contactManager, @@ -98,6 +107,7 @@ class SocialBackupManagerImpl extends BdfIncomingMessageHook this.backupMetadataParser = backupMetadataParser; this.backupMetadataEncoder = backupMetadataEncoder; this.backupPayloadEncoder = backupPayloadEncoder; + this.messageParser = messageParser; this.messageEncoder = messageEncoder; this.identityManager = identityManager; this.contactManager = contactManager; @@ -125,13 +135,21 @@ class SocialBackupManagerImpl extends BdfIncomingMessageHook Visibility client = clientVersioningManager.getClientVisibility(txn, c.getId(), CLIENT_ID, MAJOR_VERSION); db.setGroupVisibility(txn, c.getId(), g.getId(), client); - // TODO: Add the contact to our backup, if any + // Attach the contact ID to the group + setContactId(txn, g.getId(), c.getId()); + // Add the contact to our backup, if any + if (localBackupExists(txn)) { + updateBackup(txn, loadContactData(txn)); + } } @Override public void removingContact(Transaction txn, Contact c) throws DbException { db.removeGroup(txn, getContactGroup(c)); - // TODO: Remove the contact from our backup, if any + // Remove the contact from our backup, if any + if (localBackupExists(txn)) { + updateBackup(txn, loadContactData(txn)); + } } @Override @@ -147,20 +165,34 @@ class SocialBackupManagerImpl extends BdfIncomingMessageHook BdfDictionary meta) throws DbException, FormatException { MessageType type = MessageType.fromValue(body.getLong(0).intValue()); if (type == SHARD) { - // TODO: Add the shard to our backup, if any + // Only one shard should be received from each contact + if (findMessage(txn, m.getGroupId(), SHARD, false) != null) { + throw new FormatException(); + } + // Add the shard to our backup, if any + if (localBackupExists(txn)) { + Shard shard = messageParser.parseShardMessage(body); + ContactId c = getContactId(txn, m.getGroupId()); + List contactData = loadContactData(txn); + ListIterator it = contactData.listIterator(); + while (it.hasNext()) { + ContactData cd = it.next(); + if (cd.getContact().getId().equals(c)) { + it.set(new ContactData(cd.getContact(), + cd.getProperties(), shard)); + updateBackup(txn, contactData); + break; + } + } + } } else if (type == BACKUP) { // Keep the newest version of the backup, delete any older versions int version = meta.getLong(MSG_KEY_VERSION).intValue(); - BdfDictionary query = BdfDictionary.of( - new BdfEntry(MSG_KEY_MESSAGE_TYPE, BACKUP.getValue()), - new BdfEntry(MSG_KEY_LOCAL, false)); - Map results = - clientHelper.getMessageMetadataAsDictionary(txn, - m.getGroupId(), query); - if (results.size() > 1) throw new DbException(); - for (Entry e : results.entrySet()) { - MessageId prevId = e.getKey(); - BdfDictionary prevMeta = e.getValue(); + Pair prev = + findMessage(txn, m.getGroupId(), BACKUP, false); + if (prev != null) { + MessageId prevId = prev.getFirst(); + BdfDictionary prevMeta = prev.getSecond(); int prevVersion = prevMeta.getLong(MSG_KEY_VERSION).intValue(); if (version > prevVersion) { // This backup is newer - delete the previous backup @@ -192,7 +224,7 @@ class SocialBackupManagerImpl extends BdfIncomingMessageHook @Override public void createBackup(Transaction txn, List custodianIds, int threshold) throws DbException { - if (getBackupMetadata(txn) != null) throw new BackupExistsException(); + if (localBackupExists(txn)) throw new BackupExistsException(); // Load the contacts List custodians = new ArrayList<>(custodianIds.size()); for (ContactId custodianId : custodianIds) { @@ -200,7 +232,9 @@ class SocialBackupManagerImpl extends BdfIncomingMessageHook } // Create the encrypted backup payload SecretKey secret = crypto.generateSecretKey(); - BackupPayload payload = createBackupPayload(txn, secret, 0); + List contactData = loadContactData(txn); + BackupPayload payload = + createBackupPayload(txn, secret, contactData, 0); // Create the shards List shards = darkCrystal.createShards(secret, custodians.size(), threshold); @@ -232,23 +266,50 @@ class SocialBackupManagerImpl extends BdfIncomingMessageHook MAJOR_VERSION, c); } - private BackupPayload createBackupPayload(Transaction txn, - SecretKey secret, int version) throws DbException { - Identity identity = identityManager.getIdentity(txn); - Collection contacts = contactManager.getContacts(txn); - List eligible = new ArrayList<>(); - List> properties = - new ArrayList<>(); - // Include all contacts whose handshake public keys we know - for (Contact c : contacts) { - if (c.getHandshakePublicKey() != null) { - eligible.add(c); - properties.add(getTransportProperties(txn, c.getId())); - // TODO: Include shard received from contact, if any - } + private void setContactId(Transaction txn, GroupId g, ContactId c) + throws DbException { + BdfDictionary d = new BdfDictionary(); + d.put(GROUP_KEY_CONTACT_ID, c.getInt()); + try { + clientHelper.mergeGroupMetadata(txn, g, d); + } catch (FormatException e) { + throw new AssertionError(e); } + } + + private ContactId getContactId(Transaction txn, GroupId g) + throws DbException { + try { + BdfDictionary meta = + clientHelper.getGroupMetadataAsDictionary(txn, g); + return new ContactId(meta.getLong(GROUP_KEY_CONTACT_ID).intValue()); + } catch (FormatException e) { + throw new DbException(e); + } + } + + private BackupPayload createBackupPayload(Transaction txn, + SecretKey secret, List contactData, int version) + throws DbException { + Identity identity = identityManager.getIdentity(txn); return backupPayloadEncoder.encodeBackupPayload(secret, identity, - eligible, properties, version); + contactData, version); + } + + private List loadContactData(Transaction txn) + throws DbException { + Collection contacts = contactManager.getContacts(txn); + List contactData = new ArrayList<>(); + for (Contact c : contacts) { + // Skip contacts that are in the process of being removed + Group contactGroup = getContactGroup(c); + if (!db.containsGroup(txn, contactGroup.getId())) continue; + Map props = + getTransportProperties(txn, c.getId()); + Shard shard = getRemoteShard(txn, contactGroup.getId()); + contactData.add(new ContactData(c, props, shard)); + } + return contactData; } private Map getTransportProperties( @@ -284,4 +345,76 @@ class SocialBackupManagerImpl extends BdfIncomingMessageHook new BdfEntry(MSG_KEY_VERSION, version)); clientHelper.addLocalMessage(txn, m, meta, true, false); } + + private boolean localBackupExists(Transaction txn) throws DbException { + return !db.getGroupMetadata(txn, localGroup.getId()).isEmpty(); + } + + @Nullable + private Shard getRemoteShard(Transaction txn, GroupId g) + throws DbException { + try { + Pair prev = + findMessage(txn, g, SHARD, false); + if (prev == null) return null; + BdfList body = clientHelper.getMessageAsList(txn, prev.getFirst()); + return messageParser.parseShardMessage(body); + } catch (FormatException e) { + throw new DbException(e); + } + } + + private void updateBackup(Transaction txn, List contactData) + throws DbException { + BackupMetadata backupMetadata = requireNonNull(getBackupMetadata(txn)); + int newVersion = backupMetadata.getVersion() + 1; + BackupPayload payload = createBackupPayload(txn, + backupMetadata.getSecret(), contactData, newVersion); + LocalAuthor localAuthor = identityManager.getLocalAuthor(txn); + try { + for (Author author : backupMetadata.getCustodians()) { + try { + Contact custodian = contactManager.getContact(txn, + author.getId(), localAuthor.getId()); + Group contactGroup = getContactGroup(custodian); + Pair prev = findMessage(txn, + contactGroup.getId(), BACKUP, true); + if (prev != null) { + // Delete the previous backup message + MessageId prevId = prev.getFirst(); + db.deleteMessage(txn, prevId); + db.deleteMessageMetadata(txn, prevId); + } + sendBackupMessage(txn, custodian, newVersion, payload); + } catch (NoSuchContactException e) { + // The custodian is no longer a contact - continue + } + } + } catch (FormatException e) { + throw new DbException(e); + } + BdfDictionary meta = + BdfDictionary.of(new BdfEntry(GROUP_KEY_VERSION, newVersion)); + try { + clientHelper.mergeGroupMetadata(txn, localGroup.getId(), meta); + } catch (FormatException e) { + throw new AssertionError(e); + } + } + + @Nullable + private Pair findMessage(Transaction txn, + GroupId g, MessageType type, boolean local) + throws DbException, FormatException { + BdfDictionary query = BdfDictionary.of( + new BdfEntry(MSG_KEY_MESSAGE_TYPE, type.getValue()), + new BdfEntry(MSG_KEY_LOCAL, local)); + Map results = + clientHelper.getMessageMetadataAsDictionary(txn, g, query); + if (results.size() > 1) throw new DbException(); + if (results.isEmpty()) return null; + Entry e = + results.entrySet().iterator().next(); + return new Pair<>(e.getKey(), e.getValue()); + } }