diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/contact/ContactManager.java b/bramble-api/src/main/java/org/briarproject/bramble/api/contact/ContactManager.java index 5ae366c2e867c7a01930de133d77ccbd3fe5df8e..f131a1d2f4b7337a9a6779b4d22bd5ec33740d99 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/contact/ContactManager.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/contact/ContactManager.java @@ -59,17 +59,18 @@ public interface ContactManager { throws DbException; /** - * Returns the static link that needs to be sent to the contact to be added. + * Returns the handshake link that needs to be sent to a contact we want + * to add. */ String getHandshakeLink() throws DbException; /** - * Requests a new contact to be added via the given {@code link}. + * Adds a new pending contact identified by the given handshake link. * - * @param link The link received from the contact we want to add. + * @param link The handshake link received from the contact we want to add. * @param alias The alias the user has given this contact. */ - void addPendingContact(String link, String alias) + PendingContact addPendingContact(String link, String alias) throws DbException, FormatException; /** @@ -78,10 +79,9 @@ public interface ContactManager { Collection<PendingContact> getPendingContacts() throws DbException; /** - * Removes a {@link PendingContact} that is in state - * {@link PendingContactState FAILED}. + * Removes a {@link PendingContact}. */ - void removePendingContact(PendingContactId pendingContact) throws DbException; + void removePendingContact(PendingContactId p) throws DbException; /** * Returns the contact with the given ID. diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/contact/HandshakeLinkConstants.java b/bramble-api/src/main/java/org/briarproject/bramble/api/contact/HandshakeLinkConstants.java new file mode 100644 index 0000000000000000000000000000000000000000..b852c27c044b221c1c2b3e16b433d9102e89e5dd --- /dev/null +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/contact/HandshakeLinkConstants.java @@ -0,0 +1,34 @@ +package org.briarproject.bramble.api.contact; + +import java.util.regex.Pattern; + +public interface HandshakeLinkConstants { + + /** + * The current version of the handshake link format. + */ + int FORMAT_VERSION = 0; + + /** + * The length of a base32-encoded handshake link in bytes, excluding the + * 'briar://' prefix. + */ + int BASE32_LINK_BYTES = 53; + + /** + * The length of a raw handshake link in bytes, before base32 encoding. + */ + int RAW_LINK_BYTES = 33; + + /** + * Regular expression for matching handshake links, including or excluding + * the 'briar://' prefix. + */ + Pattern LINK_REGEX = + Pattern.compile("(briar://)?([a-z2-7]{" + BASE32_LINK_BYTES + "})"); + + /** + * Label for hashing handshake public keys to calculate their identifiers. + */ + String ID_LABEL = "org.briarproject.bramble/HANDSHAKE_KEY_ID"; +} diff --git a/bramble-api/src/main/java/org/briarproject/bramble/util/Base32.java b/bramble-api/src/main/java/org/briarproject/bramble/util/Base32.java index cff5c4e9901db9a20f3d8d54c49487f76ec4a392..d15292b9208a9d274852308e5ffbcadc429a2898 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/util/Base32.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/util/Base32.java @@ -37,7 +37,7 @@ public class Base32 { return s.toString(); } - public static byte[] decode(String s) { + public static byte[] decode(String s, boolean strict) { ByteArrayOutputStream b = new ByteArrayOutputStream(); int digitIndex = 0, digitCount = s.length(), currentByte = 0x00; int byteMask = 0x80, codeMask = 0x10; @@ -61,7 +61,7 @@ public class Base32 { } } // If any extra bits were used for encoding, they should all be zero - if (byteMask != 0x80 && currentByte != 0x00) + if (strict && byteMask != 0x80 && currentByte != 0x00) throw new IllegalArgumentException(); return b.toByteArray(); } diff --git a/bramble-api/src/main/java/org/briarproject/bramble/util/StringUtils.java b/bramble-api/src/main/java/org/briarproject/bramble/util/StringUtils.java index b13fb6030665d260ad7b4cf24b38028035619d89..9b6b1220c76d8beca91a76069b964456b36120c6 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/util/StringUtils.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/util/StringUtils.java @@ -153,4 +153,13 @@ public class StringUtils { return new String(c); } + public static String getRandomBase32String(int length) { + char[] c = new char[length]; + for (int i = 0; i < length; i++) { + int character = random.nextInt(32); + if (character < 26) c[i] = (char) ('a' + character); + else c[i] = (char) ('2' + (character - 26)); + } + return new String(c); + } } diff --git a/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactManagerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactManagerImpl.java index 2cf100a1415c2f53794dfcc11e35775c1edf4655..632d9226ba57598eca141d2a772e4b7af750db2d 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactManagerImpl.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactManagerImpl.java @@ -1,5 +1,6 @@ package org.briarproject.bramble.contact; +import org.briarproject.bramble.api.FormatException; import org.briarproject.bramble.api.contact.Contact; import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.contact.ContactManager; @@ -20,19 +21,19 @@ import org.briarproject.bramble.api.transport.KeyManager; import java.util.Collection; import java.util.List; -import java.util.Random; import java.util.concurrent.CopyOnWriteArrayList; import javax.annotation.Nullable; import javax.annotation.concurrent.ThreadSafe; import javax.inject.Inject; -import static java.util.Collections.emptyList; +import static org.briarproject.bramble.api.contact.HandshakeLinkConstants.BASE32_LINK_BYTES; import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH; import static org.briarproject.bramble.api.identity.AuthorInfo.Status.OURSELVES; import static org.briarproject.bramble.api.identity.AuthorInfo.Status.UNKNOWN; import static org.briarproject.bramble.api.identity.AuthorInfo.Status.UNVERIFIED; import static org.briarproject.bramble.api.identity.AuthorInfo.Status.VERIFIED; +import static org.briarproject.bramble.util.StringUtils.getRandomBase32String; import static org.briarproject.bramble.util.StringUtils.toUtf8; @ThreadSafe @@ -40,19 +41,22 @@ import static org.briarproject.bramble.util.StringUtils.toUtf8; class ContactManagerImpl implements ContactManager { private static final String REMOTE_CONTACT_LINK = - "briar://" + getRandomBase32String(LINK_LENGTH); + "briar://" + getRandomBase32String(BASE32_LINK_BYTES); private final DatabaseComponent db; private final KeyManager keyManager; private final IdentityManager identityManager; + private final PendingContactFactory pendingContactFactory; private final List<ContactHook> hooks; @Inject ContactManagerImpl(DatabaseComponent db, KeyManager keyManager, - IdentityManager identityManager) { + IdentityManager identityManager, + PendingContactFactory pendingContactFactory) { this.db = db; this.keyManager = keyManager; this.identityManager = identityManager; + this.pendingContactFactory = pendingContactFactory; hooks = new CopyOnWriteArrayList<>(); } @@ -96,34 +100,23 @@ class ContactManagerImpl implements ContactManager { return REMOTE_CONTACT_LINK; } - // TODO replace with real implementation - @SuppressWarnings("SameParameterValue") - private static String getRandomBase32String(int length) { - Random random = new Random(); - char[] c = new char[length]; - for (int i = 0; i < length; i++) { - int character = random.nextInt(32); - if (character < 26) c[i] = (char) ('a' + character); - else c[i] = (char) ('2' + (character - 26)); - } - return new String(c); - } - @Override - public void addPendingContact(String link, String alias) - throws DbException { - // TODO replace with real implementation + public PendingContact addPendingContact(String link, String alias) + throws DbException, FormatException { + PendingContact p = + pendingContactFactory.createPendingContact(link, alias); + db.transaction(false, txn -> db.addPendingContact(txn, p)); + return p; } @Override - public Collection<PendingContact> getPendingContacts() { - // TODO replace with real implementation - return emptyList(); + public Collection<PendingContact> getPendingContacts() throws DbException { + return db.transactionWithResult(true, db::getPendingContacts); } @Override - public void removePendingContact(PendingContactId id) throws DbException { - // TODO replace with real implementation + public void removePendingContact(PendingContactId p) throws DbException { + db.transaction(false, txn -> db.removePendingContact(txn, p)); } @Override diff --git a/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactModule.java b/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactModule.java index 2cb610972caac67f87f197aaafd15016fe7472d7..f31ce108aa08860ca63cebcdee5fc6eef712edd4 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactModule.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactModule.java @@ -28,4 +28,10 @@ public class ContactModule { ContactExchangeTaskImpl contactExchangeTask) { return contactExchangeTask; } + + @Provides + PendingContactFactory providePendingContactFactory( + PendingContactFactoryImpl pendingContactFactory) { + return pendingContactFactory; + } } diff --git a/bramble-core/src/main/java/org/briarproject/bramble/contact/PendingContactFactory.java b/bramble-core/src/main/java/org/briarproject/bramble/contact/PendingContactFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..eab17d59d5cf21443fb006315ddd79ac66e59f78 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/contact/PendingContactFactory.java @@ -0,0 +1,10 @@ +package org.briarproject.bramble.contact; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.contact.PendingContact; + +interface PendingContactFactory { + + PendingContact createPendingContact(String link, String alias) + throws FormatException; +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/contact/PendingContactFactoryImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/contact/PendingContactFactoryImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..36a627013c369c649ed873bb7541f1137a66c856 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/contact/PendingContactFactoryImpl.java @@ -0,0 +1,67 @@ +package org.briarproject.bramble.contact; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.contact.PendingContact; +import org.briarproject.bramble.api.contact.PendingContactId; +import org.briarproject.bramble.api.contact.PendingContactState; +import org.briarproject.bramble.api.crypto.CryptoComponent; +import org.briarproject.bramble.api.crypto.KeyParser; +import org.briarproject.bramble.api.crypto.PublicKey; +import org.briarproject.bramble.api.system.Clock; +import org.briarproject.bramble.util.Base32; + +import java.security.GeneralSecurityException; +import java.util.regex.Matcher; + +import javax.inject.Inject; + +import static java.lang.System.arraycopy; +import static org.briarproject.bramble.api.contact.HandshakeLinkConstants.FORMAT_VERSION; +import static org.briarproject.bramble.api.contact.HandshakeLinkConstants.ID_LABEL; +import static org.briarproject.bramble.api.contact.HandshakeLinkConstants.LINK_REGEX; +import static org.briarproject.bramble.api.contact.HandshakeLinkConstants.RAW_LINK_BYTES; + +class PendingContactFactoryImpl implements PendingContactFactory { + + private final CryptoComponent crypto; + private final Clock clock; + + @Inject + PendingContactFactoryImpl(CryptoComponent crypto, Clock clock) { + this.crypto = crypto; + this.clock = clock; + } + + @Override + public PendingContact createPendingContact(String link, String alias) + throws FormatException { + PublicKey publicKey = parseHandshakeLink(link); + PendingContactId id = getPendingContactId(publicKey); + long timestamp = clock.currentTimeMillis(); + return new PendingContact(id, publicKey.getEncoded(), alias, + PendingContactState.WAITING_FOR_CONNECTION, timestamp); + } + + private PublicKey parseHandshakeLink(String link) throws FormatException { + Matcher matcher = LINK_REGEX.matcher(link); + if (!matcher.matches()) throw new FormatException(); + link = matcher.group(); // Discard anything before or after the link + if (link.startsWith("briar://")) link = link.substring(8); + byte[] base32 = Base32.decode(link, false); + if (base32.length != RAW_LINK_BYTES) throw new AssertionError(); + if (base32[0] != FORMAT_VERSION) throw new FormatException(); + byte[] publicKeyBytes = new byte[base32.length - 1]; + arraycopy(base32, 1, publicKeyBytes, 0, publicKeyBytes.length); + try { + KeyParser parser = crypto.getAgreementKeyParser(); + return parser.parsePublicKey(publicKeyBytes); + } catch (GeneralSecurityException e) { + throw new FormatException(); + } + } + + private PendingContactId getPendingContactId(PublicKey publicKey) { + byte[] hash = crypto.hash(ID_LABEL, publicKey.getEncoded()); + return new PendingContactId(hash); + } +} diff --git a/bramble-core/src/test/java/org/briarproject/bramble/contact/ContactManagerImplTest.java b/bramble-core/src/test/java/org/briarproject/bramble/contact/ContactManagerImplTest.java index d1e2692641c7575f8bab95d714d6b86f411af1b3..df7463983de22b6cb8f304cc65d743cef5dbb4f1 100644 --- a/bramble-core/src/test/java/org/briarproject/bramble/contact/ContactManagerImplTest.java +++ b/bramble-core/src/test/java/org/briarproject/bramble/contact/ContactManagerImplTest.java @@ -47,6 +47,8 @@ public class ContactManagerImplTest extends BrambleMockTestCase { private final KeyManager keyManager = context.mock(KeyManager.class); private final IdentityManager identityManager = context.mock(IdentityManager.class); + private final PendingContactFactory pendingContactFactory = + context.mock(PendingContactFactory.class); private final ContactManager contactManager; private final Author remote = getAuthor(); private final LocalAuthor localAuthor = getLocalAuthor(); @@ -56,8 +58,8 @@ public class ContactManagerImplTest extends BrambleMockTestCase { private final ContactId contactId = contact.getId(); public ContactManagerImplTest() { - contactManager = - new ContactManagerImpl(db, keyManager, identityManager); + contactManager = new ContactManagerImpl(db, keyManager, + identityManager, pendingContactFactory); } @Test