Commit 58804799 authored by Torsten Grote's avatar Torsten Grote

Merge branch '1537-contact-manager-pending-contacts' into 'master'

Implement contact manager methods for pending contacts

Closes #1537

See merge request !1081
parents 33fdca4a 71d8fb20
Pipeline #3294 passed with stage
in 6 minutes and 37 seconds
package org.briarproject.bramble.api.keyagreement;
import java.io.IOException;
package org.briarproject.bramble.api;
/**
* Thrown when a QR code that has been scanned uses a protocol version that is
* not supported.
* Thrown when data being parsed uses a protocol or format version that is not
* supported.
*/
public class UnsupportedVersionException extends IOException {
public class UnsupportedVersionException extends FormatException {
private final boolean tooOld;
......
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.
*/
void addPendingContact(String link, String alias)
* @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
*/
PendingContact addPendingContact(String link, String alias)
throws DbException, FormatException;
/**
......@@ -78,10 +81,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.
......@@ -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;
......
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";
}
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);
}
}
......@@ -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);
}
}
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
......
......@@ -28,4 +28,10 @@ public class ContactModule {
ContactExchangeTaskImpl contactExchangeTask) {
return contactExchangeTask;
}
@Provides
PendingContactFactory providePendingContactFactory(
PendingContactFactoryImpl pendingContactFactory) {
return pendingContactFactory;
}
}
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;
}
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);
}
}
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;
......
......@@ -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
......
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;
}
}
......@@ -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;
......
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));
}