diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/UnsupportedVersionException.java b/bramble-api/src/main/java/org/briarproject/bramble/api/UnsupportedVersionException.java new file mode 100644 index 000000000..827f33e16 --- /dev/null +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/UnsupportedVersionException.java @@ -0,0 +1,18 @@ +package org.briarproject.bramble.api; + +/** + * Thrown when data being parsed uses a protocol or format version that is not + * supported. + */ +public class UnsupportedVersionException extends FormatException { + + private final boolean tooOld; + + public UnsupportedVersionException(boolean tooOld) { + this.tooOld = tooOld; + } + + public boolean isTooOld() { + return tooOld; + } +} 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 5ae366c2e..a2e2ef78c 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 @@ -1,8 +1,10 @@ package org.briarproject.bramble.api.contact; import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.UnsupportedVersionException; import org.briarproject.bramble.api.crypto.SecretKey; 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.AuthorId; @@ -11,17 +13,12 @@ import org.briarproject.bramble.api.lifecycle.LifecycleManager; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import java.util.Collection; -import java.util.regex.Pattern; import javax.annotation.Nullable; @NotNullByDefault public interface ContactManager { - int LINK_LENGTH = 64; - Pattern LINK_REGEX = - Pattern.compile("(briar://)?([a-z2-7]{" + LINK_LENGTH + "})"); - /** * Registers a hook to be called whenever a contact is added or removed. * This method should be called before @@ -59,17 +56,23 @@ 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}. + * Creates a {@link PendingContact} from the given handshake link and + * alias, adds it to the database and returns it. * - * @param link The link received from the contact we want to add. - * @param alias The alias the user has given this contact. + * @param link The handshake link received from the contact we want to add + * @param alias The alias the user has given this contact + * @return A PendingContact representing the contact to be added + * @throws UnsupportedVersionException If the link uses a format version + * that is not supported + * @throws FormatException If the link is invalid */ - void addPendingContact(String link, String alias) + PendingContact addPendingContact(String link, String alias) throws DbException, FormatException; /** @@ -78,10 +81,9 @@ public interface ContactManager { Collection 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. @@ -92,7 +94,7 @@ public interface ContactManager { * Returns the contact with the given remoteAuthorId * that was added by the LocalAuthor with the given localAuthorId * - * @throws org.briarproject.bramble.api.db.NoSuchContactException + * @throws NoSuchContactException If the contact is not in the database */ Contact getContact(AuthorId remoteAuthorId, AuthorId localAuthorId) throws DbException; @@ -101,7 +103,7 @@ public interface ContactManager { * Returns the contact with the given remoteAuthorId * that was added by the LocalAuthor with the given localAuthorId * - * @throws org.briarproject.bramble.api.db.NoSuchContactException + * @throws NoSuchContactException If the contact is not in the database */ Contact getContact(Transaction txn, AuthorId remoteAuthorId, AuthorId localAuthorId) throws DbException; 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 000000000..b852c27c0 --- /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/api/keyagreement/UnsupportedVersionException.java b/bramble-api/src/main/java/org/briarproject/bramble/api/keyagreement/UnsupportedVersionException.java deleted file mode 100644 index 3f861d83b..000000000 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/keyagreement/UnsupportedVersionException.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.briarproject.bramble.api.keyagreement; - -import java.io.IOException; - -/** - * Thrown when a QR code that has been scanned uses a protocol version that is - * not supported. - */ -public class UnsupportedVersionException extends IOException { - - private final boolean tooOld; - - public UnsupportedVersionException(boolean tooOld) { - this.tooOld = tooOld; - } - - public boolean isTooOld() { - return tooOld; - } -} 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 new file mode 100644 index 000000000..d15292b92 --- /dev/null +++ b/bramble-api/src/main/java/org/briarproject/bramble/util/Base32.java @@ -0,0 +1,75 @@ +package org.briarproject.bramble.util; + +import java.io.ByteArrayOutputStream; + +public class Base32 { + + private static final char[] DIGITS = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', + 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', + 'Y', 'Z', '2', '3', '4', '5', '6', '7' + }; + + public static String encode(byte[] b) { + StringBuilder s = new StringBuilder(); + int byteIndex = 0, currentCode = 0x00; + int byteMask = 0x80, codeMask = 0x10; + while (byteIndex < b.length) { + if ((b[byteIndex] & byteMask) != 0) currentCode |= codeMask; + // After every 8 bits, move on to the next byte + if (byteMask == 0x01) { + byteMask = 0x80; + byteIndex++; + } else { + byteMask >>>= 1; + } + // After every 5 bits, move on to the next digit + if (codeMask == 0x01) { + s.append(DIGITS[currentCode]); + codeMask = 0x10; + currentCode = 0x00; + } else { + codeMask >>>= 1; + } + } + // If we're part-way through a digit, output it + if (codeMask != 0x10) s.append(DIGITS[currentCode]); + return s.toString(); + } + + 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; + while (digitIndex < digitCount) { + int code = decodeDigit(s.charAt(digitIndex)); + if ((code & codeMask) != 0) currentByte |= byteMask; + // After every 8 bits, move on to the next byte + if (byteMask == 0x01) { + b.write(currentByte); + byteMask = 0x80; + currentByte = 0x00; + } else { + byteMask >>>= 1; + } + // After every 5 bits, move on to the next digit + if (codeMask == 0x01) { + codeMask = 0x10; + digitIndex++; + } else { + codeMask >>>= 1; + } + } + // If any extra bits were used for encoding, they should all be zero + if (strict && byteMask != 0x80 && currentByte != 0x00) + throw new IllegalArgumentException(); + return b.toByteArray(); + } + + private static int decodeDigit(char c) { + if (c >= 'A' && c <= 'Z') return c - 'A'; + if (c >= 'a' && c <= 'z') return c - 'a'; + if (c >= '2' && c <= '7') return c - '2' + 26; + throw new IllegalArgumentException("Not a base32 digit: " + c); + } +} 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 b13fb6030..9b6b1220c 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 2cf100a14..632d9226b 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 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 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 void addPendingContact(String link, String alias) - throws DbException { - // TODO replace with real implementation + public Collection getPendingContacts() throws DbException { + return db.transactionWithResult(true, db::getPendingContacts); } @Override - public Collection getPendingContacts() { - // TODO replace with real implementation - return emptyList(); - } - - @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 2cb610972..f31ce108a 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 000000000..cfa555565 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/contact/PendingContactFactory.java @@ -0,0 +1,18 @@ +package org.briarproject.bramble.contact; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.UnsupportedVersionException; +import org.briarproject.bramble.api.contact.PendingContact; + +interface PendingContactFactory { + + /** + * Creates a {@link PendingContact} from the given handshake link and alias. + * + * @throws UnsupportedVersionException If the link uses a format version + * that is not supported + * @throws FormatException If the link is invalid + */ + 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 000000000..a901cb60d --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/contact/PendingContactFactoryImpl.java @@ -0,0 +1,70 @@ +package org.briarproject.bramble.contact; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.UnsupportedVersionException; +import org.briarproject.bramble.api.contact.PendingContact; +import org.briarproject.bramble.api.contact.PendingContactId; +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; +import static org.briarproject.bramble.api.contact.PendingContactState.WAITING_FOR_CONNECTION; + +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, + WAITING_FOR_CONNECTION, timestamp); + } + + private PublicKey parseHandshakeLink(String link) throws FormatException { + Matcher matcher = LINK_REGEX.matcher(link); + if (!matcher.find()) throw new FormatException(); + // Discard 'briar://' and anything before or after the link + link = matcher.group(2); + byte[] base32 = Base32.decode(link, false); + if (base32.length != RAW_LINK_BYTES) throw new AssertionError(); + byte version = base32[0]; + if (version != FORMAT_VERSION) + throw new UnsupportedVersionException(version < FORMAT_VERSION); + 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/main/java/org/briarproject/bramble/keyagreement/PayloadParserImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/keyagreement/PayloadParserImpl.java index 2f6096760..f574360a7 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/keyagreement/PayloadParserImpl.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/keyagreement/PayloadParserImpl.java @@ -1,13 +1,13 @@ package org.briarproject.bramble.keyagreement; import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.UnsupportedVersionException; import org.briarproject.bramble.api.data.BdfList; import org.briarproject.bramble.api.data.BdfReader; import org.briarproject.bramble.api.data.BdfReaderFactory; import org.briarproject.bramble.api.keyagreement.Payload; import org.briarproject.bramble.api.keyagreement.PayloadParser; import org.briarproject.bramble.api.keyagreement.TransportDescriptor; -import org.briarproject.bramble.api.keyagreement.UnsupportedVersionException; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.plugin.BluetoothConstants; import org.briarproject.bramble.api.plugin.LanTcpConstants; 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 d1e269264..df7463983 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 diff --git a/bramble-core/src/test/java/org/briarproject/bramble/contact/PendingContactFactoryImplTest.java b/bramble-core/src/test/java/org/briarproject/bramble/contact/PendingContactFactoryImplTest.java new file mode 100644 index 000000000..c575357cc --- /dev/null +++ b/bramble-core/src/test/java/org/briarproject/bramble/contact/PendingContactFactoryImplTest.java @@ -0,0 +1,129 @@ +package org.briarproject.bramble.contact; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.UnsupportedVersionException; +import org.briarproject.bramble.api.contact.PendingContact; +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.test.BrambleMockTestCase; +import org.briarproject.bramble.util.Base32; +import org.jmock.Expectations; +import org.junit.Test; + +import java.security.GeneralSecurityException; + +import static java.lang.System.arraycopy; +import static org.briarproject.bramble.api.contact.HandshakeLinkConstants.BASE32_LINK_BYTES; +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.RAW_LINK_BYTES; +import static org.briarproject.bramble.api.contact.PendingContactState.WAITING_FOR_CONNECTION; +import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH; +import static org.briarproject.bramble.test.TestUtils.getRandomBytes; +import static org.briarproject.bramble.test.TestUtils.getRandomId; +import static org.briarproject.bramble.util.StringUtils.getRandomString; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.fail; + +public class PendingContactFactoryImplTest extends BrambleMockTestCase { + + private final CryptoComponent crypto = context.mock(CryptoComponent.class); + private final Clock clock = context.mock(Clock.class); + private final KeyParser keyParser = context.mock(KeyParser.class); + private final PublicKey publicKey = context.mock(PublicKey.class); + + private final PendingContactFactory pendingContactFactory = + new PendingContactFactoryImpl(crypto, clock); + private final String alias = getRandomString(MAX_AUTHOR_NAME_LENGTH); + private final byte[] publicKeyBytes = getRandomBytes(RAW_LINK_BYTES - 1); + private final byte[] idBytes = getRandomId(); + private final long timestamp = System.currentTimeMillis(); + + @Test(expected = FormatException.class) + public void testRejectsSyntacticallyInvalidLink() throws Exception { + pendingContactFactory.createPendingContact("briar://potato", alias); + } + + @Test + public void testRejectsLinkWithUnknownFormatVersion() throws Exception { + String link = encodeLink(FORMAT_VERSION + 1); + try { + pendingContactFactory.createPendingContact(link, alias); + fail(); + } catch (UnsupportedVersionException e) { + assertFalse(e.isTooOld()); + } + } + + @Test(expected = FormatException.class) + public void testRejectsLinkWithInvalidPublicKey() throws Exception { + context.checking(new Expectations() {{ + oneOf(crypto).getAgreementKeyParser(); + will(returnValue(keyParser)); + oneOf(keyParser).parsePublicKey(with(equal(publicKeyBytes))); + will(throwException(new GeneralSecurityException())); + }}); + + pendingContactFactory.createPendingContact(encodeLink(), alias); + } + + @Test + public void testAcceptsValidLinkWithoutPrefix() throws Exception { + testAcceptsValidLink(encodeLink()); + } + + @Test + public void testAcceptsValidLinkWithPrefix() throws Exception { + testAcceptsValidLink("briar://" + encodeLink()); + } + + @Test + public void testAcceptsValidLinkWithRubbish() throws Exception { + testAcceptsValidLink("before " + encodeLink() + " after"); + } + + @Test + public void testAcceptsValidLinkWithPrefixAndRubbish() throws Exception { + testAcceptsValidLink("before briar://" + encodeLink() + " after"); + } + + private void testAcceptsValidLink(String link) throws Exception { + context.checking(new Expectations() {{ + oneOf(crypto).getAgreementKeyParser(); + will(returnValue(keyParser)); + oneOf(keyParser).parsePublicKey(with(equal(publicKeyBytes))); + will(returnValue(publicKey)); + allowing(publicKey).getEncoded(); + will(returnValue(publicKeyBytes)); + oneOf(crypto).hash(ID_LABEL, publicKeyBytes); + will(returnValue(idBytes)); + oneOf(clock).currentTimeMillis(); + will(returnValue(timestamp)); + }}); + + PendingContact p = + pendingContactFactory.createPendingContact(link, alias); + assertArrayEquals(idBytes, p.getId().getBytes()); + assertArrayEquals(publicKeyBytes, p.getPublicKey()); + assertEquals(alias, p.getAlias()); + assertEquals(WAITING_FOR_CONNECTION, p.getState()); + assertEquals(timestamp, p.getTimestamp()); + } + + private String encodeLink() { + return encodeLink(FORMAT_VERSION); + } + + private String encodeLink(int formatVersion) { + byte[] rawLink = new byte[RAW_LINK_BYTES]; + rawLink[0] = (byte) formatVersion; + arraycopy(publicKeyBytes, 0, rawLink, 1, publicKeyBytes.length); + String base32 = Base32.encode(rawLink).toLowerCase(); + assertEquals(BASE32_LINK_BYTES, base32.length()); + return base32; + } +} diff --git a/bramble-core/src/test/java/org/briarproject/bramble/keyagreement/PayloadParserImplTest.java b/bramble-core/src/test/java/org/briarproject/bramble/keyagreement/PayloadParserImplTest.java index 3b072547f..f9d74e081 100644 --- a/bramble-core/src/test/java/org/briarproject/bramble/keyagreement/PayloadParserImplTest.java +++ b/bramble-core/src/test/java/org/briarproject/bramble/keyagreement/PayloadParserImplTest.java @@ -2,11 +2,11 @@ package org.briarproject.bramble.keyagreement; import org.briarproject.bramble.api.Bytes; import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.UnsupportedVersionException; import org.briarproject.bramble.api.data.BdfList; import org.briarproject.bramble.api.data.BdfReader; import org.briarproject.bramble.api.data.BdfReaderFactory; import org.briarproject.bramble.api.keyagreement.Payload; -import org.briarproject.bramble.api.keyagreement.UnsupportedVersionException; import org.briarproject.bramble.test.BrambleMockTestCase; import org.jmock.Expectations; import org.junit.Test; diff --git a/bramble-core/src/test/java/org/briarproject/bramble/util/Base32Test.java b/bramble-core/src/test/java/org/briarproject/bramble/util/Base32Test.java new file mode 100644 index 000000000..41b5824e4 --- /dev/null +++ b/bramble-core/src/test/java/org/briarproject/bramble/util/Base32Test.java @@ -0,0 +1,71 @@ +package org.briarproject.bramble.util; + +import org.briarproject.bramble.test.BrambleTestCase; +import org.junit.Test; + +import java.util.Random; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +public class Base32Test extends BrambleTestCase { + + // Test vectors from RFC 4648 + // https://tools.ietf.org/html/rfc4648#section-10 + + @Test + public void testEncoding() { + assertEquals("", Base32.encode(new byte[0])); + assertEquals("MY", Base32.encode(new byte[] {'f'})); + assertEquals("MZXQ", Base32.encode(new byte[] {'f', 'o'})); + assertEquals("MZXW6", Base32.encode(new byte[] {'f', 'o', 'o'})); + assertEquals("MZXW6YQ", Base32.encode(new byte[] {'f', 'o', 'o', 'b'})); + assertEquals("MZXW6YTB", + Base32.encode(new byte[] {'f', 'o', 'o', 'b', 'a'})); + assertEquals("MZXW6YTBOI", + Base32.encode(new byte[] {'f', 'o', 'o', 'b', 'a', 'r'})); + } + + @Test + public void testStrictDecoding() { + testDecoding(true); + } + + @Test + public void testNonStrictDecoding() { + testDecoding(false); + } + + private void testDecoding(boolean strict) { + assertArrayEquals(new byte[0], Base32.decode("", strict)); + assertArrayEquals(new byte[] {'f'}, Base32.decode("MY", strict)); + assertArrayEquals(new byte[] {'f', 'o'}, Base32.decode("MZXQ", strict)); + assertArrayEquals(new byte[] {'f', 'o', 'o'}, + Base32.decode("MZXW6", strict)); + assertArrayEquals(new byte[] {'f', 'o', 'o', 'b'}, + Base32.decode("MZXW6YQ", strict)); + assertArrayEquals(new byte[] {'f', 'o', 'o', 'b', 'a'}, + Base32.decode("MZXW6YTB", strict)); + assertArrayEquals(new byte[] {'f', 'o', 'o', 'b', 'a', 'r'}, + Base32.decode("MZXW6YTBOI", strict)); + } + + @Test(expected = IllegalArgumentException.class) + public void testStrictDecodingRejectsNonZeroUnusedBits() { + Base32.decode("MZ", true); + } + + @Test + public void testNonStrictDecodingAcceptsNonZeroUnusedBits() { + assertArrayEquals(new byte[] {'f'}, Base32.decode("MZ", false)); + } + + @Test + public void testRoundTrip() { + Random random = new Random(); + byte[] data = new byte[100 + random.nextInt(100)]; + random.nextBytes(data); + assertArrayEquals(data, Base32.decode(Base32.encode(data), true)); + assertArrayEquals(data, Base32.decode(Base32.encode(data), false)); + } +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/AddContactViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/AddContactViewModel.java index 729201cfd..6ed6087e3 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/AddContactViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/AddContactViewModel.java @@ -22,7 +22,7 @@ import javax.inject.Inject; import static java.util.logging.Level.WARNING; import static java.util.logging.Logger.getLogger; -import static org.briarproject.bramble.api.contact.ContactManager.LINK_REGEX; +import static org.briarproject.bramble.api.contact.HandshakeLinkConstants.LINK_REGEX; import static org.briarproject.bramble.util.LogUtils.logException; @NotNullByDefault @@ -105,7 +105,7 @@ public class AddContactViewModel extends AndroidViewModel { }); } - public LiveData getAddContactResult() { + LiveData getAddContactResult() { return addContactResult; } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/LinkExchangeFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/LinkExchangeFragment.java index 60450c1f5..e37d987c6 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/LinkExchangeFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/LinkExchangeFragment.java @@ -29,7 +29,7 @@ import javax.inject.Inject; import static android.content.Context.CLIPBOARD_SERVICE; import static android.widget.Toast.LENGTH_SHORT; import static java.util.Objects.requireNonNull; -import static org.briarproject.bramble.api.contact.ContactManager.LINK_REGEX; +import static org.briarproject.bramble.api.contact.HandshakeLinkConstants.LINK_REGEX; import static org.briarproject.briar.android.util.UiUtils.observeOnce; @MethodsNotNullByDefault diff --git a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/KeyAgreementFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/KeyAgreementFragment.java index 168708367..e2b449126 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/KeyAgreementFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/KeyAgreementFragment.java @@ -15,6 +15,7 @@ import android.widget.Toast; import com.google.zxing.Result; +import org.briarproject.bramble.api.UnsupportedVersionException; import org.briarproject.bramble.api.event.Event; import org.briarproject.bramble.api.event.EventBus; import org.briarproject.bramble.api.keyagreement.KeyAgreementResult; @@ -22,7 +23,6 @@ import org.briarproject.bramble.api.keyagreement.KeyAgreementTask; import org.briarproject.bramble.api.keyagreement.Payload; import org.briarproject.bramble.api.keyagreement.PayloadEncoder; import org.briarproject.bramble.api.keyagreement.PayloadParser; -import org.briarproject.bramble.api.keyagreement.UnsupportedVersionException; import org.briarproject.bramble.api.keyagreement.event.KeyAgreementAbortedEvent; import org.briarproject.bramble.api.keyagreement.event.KeyAgreementFailedEvent; import org.briarproject.bramble.api.keyagreement.event.KeyAgreementFinishedEvent;