From d6bbe59d3ac181f26af79081200adff49d39b50c Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 16 Feb 2022 15:50:12 -0300 Subject: [PATCH] Implement backend for pairing mailbox --- .../bramble/api/mailbox/MailboxManager.java | 30 ++++ .../api/mailbox/MailboxPairingState.java | 59 +++++++ .../api/mailbox/MailboxPairingTask.java | 21 +++ .../bramble/mailbox/MailboxApi.java | 4 + .../bramble/mailbox/MailboxApiImpl.java | 3 +- .../bramble/mailbox/MailboxManagerImpl.java | 72 ++++++++ .../bramble/mailbox/MailboxModule.java | 20 +++ .../mailbox/MailboxPairingTaskFactory.java | 12 ++ .../MailboxPairingTaskFactoryImpl.java | 44 +++++ .../mailbox/MailboxPairingTaskImpl.java | 166 ++++++++++++++++++ 10 files changed, 429 insertions(+), 2 deletions(-) create mode 100644 bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxManager.java create mode 100644 bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxPairingState.java create mode 100644 bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxPairingTask.java create mode 100644 bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxManagerImpl.java create mode 100644 bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxPairingTaskFactory.java create mode 100644 bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxPairingTaskFactoryImpl.java create mode 100644 bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxPairingTaskImpl.java diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxManager.java b/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxManager.java new file mode 100644 index 000000000..3fbdda57a --- /dev/null +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxManager.java @@ -0,0 +1,30 @@ +package org.briarproject.bramble.api.mailbox; + +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.db.Transaction; + +import javax.annotation.Nullable; + +public interface MailboxManager { + + /** + * @return true if a mailbox is already paired. + */ + boolean isPaired(Transaction txn) throws DbException; + + /** + * Returns the currently running pairing task, + * or null if no pairing task is running. + */ + @Nullable + MailboxPairingTask getCurrentPairingTask(); + + /** + * Starts and returns a pairing task. If a pairing task is already running, + * it will be returned and the argument will be ignored. + * + * @param qrCodePayload The ISO-8859-1 encoded bytes of the mailbox QR code. + */ + MailboxPairingTask startPairingTask(String qrCodePayload); + +} diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxPairingState.java b/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxPairingState.java new file mode 100644 index 000000000..f2f1cc948 --- /dev/null +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxPairingState.java @@ -0,0 +1,59 @@ +package org.briarproject.bramble.api.mailbox; + +import javax.annotation.Nullable; + +public abstract class MailboxPairingState { + + /** + * The QR code payload that was scanned by the user. + * This is null if the code should not be re-used anymore in this state. + */ + @Nullable + public final String qrCodePayload; + + MailboxPairingState(@Nullable String qrCodePayload) { + this.qrCodePayload = qrCodePayload; + } + + public static class QrCodeReceived extends MailboxPairingState { + public QrCodeReceived(String qrCodePayload) { + super(qrCodePayload); + } + } + + public static class Pairing extends MailboxPairingState { + public Pairing(String qrCodePayload) { + super(qrCodePayload); + } + } + + public static class Paired extends MailboxPairingState { + public Paired() { + super(null); + } + } + + public static class InvalidQrCode extends MailboxPairingState { + public InvalidQrCode() { + super(null); + } + } + + public static class MailboxAlreadyPaired extends MailboxPairingState { + public MailboxAlreadyPaired() { + super(null); + } + } + + public static class ConnectionError extends MailboxPairingState { + public ConnectionError(String qrCodePayload) { + super(qrCodePayload); + } + } + + public static class AssertionError extends MailboxPairingState { + public AssertionError(String qrCodePayload) { + super(qrCodePayload); + } + } +} diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxPairingTask.java b/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxPairingTask.java new file mode 100644 index 000000000..0f8cd05d7 --- /dev/null +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxPairingTask.java @@ -0,0 +1,21 @@ +package org.briarproject.bramble.api.mailbox; + +import org.briarproject.bramble.api.Consumer; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +@NotNullByDefault +public interface MailboxPairingTask extends Runnable { + + /** + * Adds an observer to the task. The observer will be notified on the + * event thread of the current state of the task and any subsequent state + * changes. + */ + void addObserver(Consumer observer); + + /** + * Removes an observer from the task. + */ + void removeObserver(Consumer observer); + +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxApi.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxApi.java index 82a628766..b079a6104 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxApi.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxApi.java @@ -155,6 +155,10 @@ interface MailboxApi { class ApiException extends Exception { } + @Immutable + class MailboxAlreadyPairedException extends ApiException { + } + /** * A failure that does not need to be retried, * e.g. when adding a contact that already exists. diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxApiImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxApiImpl.java index 9c80bdda7..0bf0bef47 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxApiImpl.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxApiImpl.java @@ -65,8 +65,7 @@ class MailboxApiImpl implements MailboxApi { .build(); OkHttpClient client = httpClientProvider.get(); Response response = client.newCall(request).execute(); - // TODO consider throwing a special exception for the 401 case - if (response.code() == 401) throw new ApiException(); + if (response.code() == 401) throw new MailboxAlreadyPairedException(); if (!response.isSuccessful()) throw new ApiException(); ResponseBody body = response.body(); if (body == null) throw new ApiException(); diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxManagerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxManagerImpl.java new file mode 100644 index 000000000..16a6dccde --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxManagerImpl.java @@ -0,0 +1,72 @@ +package org.briarproject.bramble.mailbox; + +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.db.Transaction; +import org.briarproject.bramble.api.lifecycle.IoExecutor; +import org.briarproject.bramble.api.mailbox.MailboxManager; +import org.briarproject.bramble.api.mailbox.MailboxPairingTask; +import org.briarproject.bramble.api.mailbox.MailboxSettingsManager; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +import java.util.concurrent.Executor; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.GuardedBy; +import javax.annotation.concurrent.Immutable; +import javax.inject.Inject; + +@Immutable +@NotNullByDefault +class MailboxManagerImpl implements MailboxManager { + + private final Executor ioExecutor; + private final MailboxSettingsManager mailboxSettingsManager; + private final MailboxPairingTaskFactory pairingTaskFactory; + private final Object lock = new Object(); + + @Nullable + @GuardedBy("lock") + private MailboxPairingTask pairingTask = null; + + @Inject + MailboxManagerImpl( + @IoExecutor Executor ioExecutor, + MailboxSettingsManager mailboxSettingsManager, + MailboxPairingTaskFactory pairingTaskFactory) { + this.ioExecutor = ioExecutor; + this.mailboxSettingsManager = mailboxSettingsManager; + this.pairingTaskFactory = pairingTaskFactory; + } + + @Override + public boolean isPaired(Transaction txn) throws DbException { + return mailboxSettingsManager.getOwnMailboxProperties(txn) != null; + } + + @Nullable + @Override + public MailboxPairingTask getCurrentPairingTask() { + synchronized (lock) { + return pairingTask; + } + } + + @Override + public MailboxPairingTask startPairingTask(String payload) { + MailboxPairingTask created; + synchronized (lock) { + if (pairingTask != null) return pairingTask; + created = pairingTaskFactory.createPairingTask(payload); + pairingTask = created; + } + ioExecutor.execute(() -> { + created.run(); + synchronized (lock) { + // remove task after it finished + pairingTask = null; + } + }); + return created; + } + +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxModule.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxModule.java index a01b15098..85e925202 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxModule.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxModule.java @@ -1,16 +1,36 @@ package org.briarproject.bramble.mailbox; +import org.briarproject.bramble.api.mailbox.MailboxManager; import org.briarproject.bramble.api.mailbox.MailboxSettingsManager; +import javax.inject.Singleton; + import dagger.Module; import dagger.Provides; @Module public class MailboxModule { + @Provides + @Singleton + MailboxManager providesMailboxManager(MailboxManagerImpl mailboxManager) { + return mailboxManager; + } + + @Provides + MailboxPairingTaskFactory provideMailboxPairingTaskFactory( + MailboxPairingTaskFactoryImpl mailboxPairingTaskFactory) { + return mailboxPairingTaskFactory; + } + @Provides MailboxSettingsManager provideMailboxSettingsManager( MailboxSettingsManagerImpl mailboxSettingsManager) { return mailboxSettingsManager; } + + @Provides + MailboxApi providesMailboxApi(MailboxApiImpl mailboxApi) { + return mailboxApi; + } } diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxPairingTaskFactory.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxPairingTaskFactory.java new file mode 100644 index 000000000..749a7f31c --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxPairingTaskFactory.java @@ -0,0 +1,12 @@ +package org.briarproject.bramble.mailbox; + + +import org.briarproject.bramble.api.mailbox.MailboxPairingTask; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +@NotNullByDefault +interface MailboxPairingTaskFactory { + + MailboxPairingTask createPairingTask(String qrCodePayload); + +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxPairingTaskFactoryImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxPairingTaskFactoryImpl.java new file mode 100644 index 000000000..9cbee44a6 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxPairingTaskFactoryImpl.java @@ -0,0 +1,44 @@ +package org.briarproject.bramble.mailbox; + +import org.briarproject.bramble.api.crypto.CryptoComponent; +import org.briarproject.bramble.api.db.TransactionManager; +import org.briarproject.bramble.api.event.EventExecutor; +import org.briarproject.bramble.api.mailbox.MailboxPairingTask; +import org.briarproject.bramble.api.mailbox.MailboxSettingsManager; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +import java.util.concurrent.Executor; + +import javax.annotation.concurrent.Immutable; +import javax.inject.Inject; + +@Immutable +@NotNullByDefault +class MailboxPairingTaskFactoryImpl implements MailboxPairingTaskFactory { + + private final Executor eventExecutor; + private final TransactionManager db; + private final CryptoComponent crypto; + private final MailboxApi api; + private final MailboxSettingsManager mailboxSettingsManager; + + @Inject + MailboxPairingTaskFactoryImpl( + @EventExecutor Executor eventExecutor, + TransactionManager db, + CryptoComponent crypto, + MailboxApi api, + MailboxSettingsManager mailboxSettingsManager) { + this.eventExecutor = eventExecutor; + this.db = db; + this.crypto = crypto; + this.api = api; + this.mailboxSettingsManager = mailboxSettingsManager; + } + + @Override + public MailboxPairingTask createPairingTask(String qrCodePayload) { + return new MailboxPairingTaskImpl(qrCodePayload, eventExecutor, db, + crypto, api, mailboxSettingsManager); + } +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxPairingTaskImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxPairingTaskImpl.java new file mode 100644 index 000000000..479b59f4d --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxPairingTaskImpl.java @@ -0,0 +1,166 @@ +package org.briarproject.bramble.mailbox; + +import org.briarproject.bramble.api.Consumer; +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.crypto.CryptoComponent; +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.db.TransactionManager; +import org.briarproject.bramble.api.event.EventExecutor; +import org.briarproject.bramble.api.mailbox.MailboxAuthToken; +import org.briarproject.bramble.api.mailbox.MailboxPairingState; +import org.briarproject.bramble.api.mailbox.MailboxPairingTask; +import org.briarproject.bramble.api.mailbox.MailboxProperties; +import org.briarproject.bramble.api.mailbox.MailboxSettingsManager; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.mailbox.MailboxApi.ApiException; +import org.briarproject.bramble.mailbox.MailboxApi.MailboxAlreadyPairedException; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.logging.Logger; + +import javax.annotation.concurrent.GuardedBy; +import javax.annotation.concurrent.ThreadSafe; + +import static java.util.logging.Level.WARNING; +import static java.util.logging.Logger.getLogger; +import static org.briarproject.bramble.util.LogUtils.logException; + +@ThreadSafe +@NotNullByDefault +class MailboxPairingTaskImpl implements MailboxPairingTask { + + private final static Logger LOG = + getLogger(MailboxPairingTaskImpl.class.getName()); + @SuppressWarnings("CharsetObjectCanBeUsed") // Requires minSdkVersion >= 19 + private static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1"); + private static final int VERSION_REQUIRED = 32; + + private final String payload; + private final Executor eventExecutor; + private final TransactionManager db; + private final CryptoComponent crypto; + private final MailboxApi api; + private final MailboxSettingsManager mailboxSettingsManager; + + private final Object lock = new Object(); + @GuardedBy("lock") + private final List> observers = + new ArrayList<>(); + @GuardedBy("lock") + private MailboxPairingState state; + + MailboxPairingTaskImpl( + String payload, + @EventExecutor Executor eventExecutor, + TransactionManager db, + CryptoComponent crypto, + MailboxApi api, + MailboxSettingsManager mailboxSettingsManager) { + this.payload = payload; + this.eventExecutor = eventExecutor; + this.db = db; + this.crypto = crypto; + this.api = api; + this.mailboxSettingsManager = mailboxSettingsManager; + state = new MailboxPairingState.QrCodeReceived(payload); + } + + @Override + public void addObserver(Consumer o) { + MailboxPairingState state; + synchronized (lock) { + observers.add(o); + state = this.state; + eventExecutor.execute(() -> o.accept(state)); + } + } + + @Override + public void removeObserver(Consumer o) { + synchronized (lock) { + observers.remove(o); + } + } + + @Override + public void run() { + try { + pairMailbox(); + } catch (FormatException e) { + onMailboxError(e, new MailboxPairingState.InvalidQrCode()); + } catch (MailboxAlreadyPairedException e) { + onMailboxError(e, new MailboxPairingState.MailboxAlreadyPaired()); + } catch (IOException e) { + onMailboxError(e, new MailboxPairingState.ConnectionError(payload)); + } catch (ApiException | DbException e) { + onMailboxError(e, new MailboxPairingState.AssertionError(payload)); + } + } + + private void pairMailbox() throws IOException, ApiException, DbException { + MailboxProperties mailboxProperties = decodeQrCodePayload(payload); + synchronized (lock) { + this.state = new MailboxPairingState.Pairing(payload); + notifyObservers(); + } + MailboxAuthToken ownerToken = api.setup(mailboxProperties); + MailboxProperties ownerProperties = new MailboxProperties( + mailboxProperties.getOnionAddress(), ownerToken, true); + db.transaction(false, txn -> mailboxSettingsManager + .setOwnMailboxProperties(txn, ownerProperties)); + synchronized (lock) { + this.state = new MailboxPairingState.Paired(); + notifyObservers(); + } + // TODO already do mailboxSettingsManager.setOwnMailboxStatus() ? + } + + private void onMailboxError(Exception e, MailboxPairingState state) { + logException(LOG, WARNING, e); + synchronized (lock) { + this.state = state; + notifyObservers(); + } + } + + @GuardedBy("lock") + private void notifyObservers() { + List> observers = + new ArrayList<>(this.observers); + MailboxPairingState state = this.state; + eventExecutor.execute(() -> { + for (Consumer o : observers) o.accept(state); + }); + } + + private MailboxProperties decodeQrCodePayload(String payload) + throws FormatException { + byte[] bytes = payload.getBytes(ISO_8859_1); + if (bytes.length != 65) { + if (LOG.isLoggable(WARNING)) { + LOG.warning("QR code length is not 65: " + bytes.length); + } + throw new FormatException(); + } + int version = bytes[0] & 0xFF; + if (version != VERSION_REQUIRED) { + if (LOG.isLoggable(WARNING)) { + LOG.warning("QR code has not version " + VERSION_REQUIRED + + ": " + version); + } + throw new FormatException(); + } + LOG.info("QR code is valid"); + byte[] onionPubKey = Arrays.copyOfRange(bytes, 1, 33); + String onionAddress = crypto.encodeOnionAddress(onionPubKey); + byte[] tokenBytes = Arrays.copyOfRange(bytes, 33, 65); + MailboxAuthToken setupToken = new MailboxAuthToken(tokenBytes); + return new MailboxProperties(onionAddress, setupToken, true); + } + +}