Partial implementation of the invitation protocol (untested).

This commit is contained in:
akwizgran
2012-02-23 23:18:25 +00:00
parent 34cd8cddc3
commit c316ebcf7a
24 changed files with 586 additions and 74 deletions

View File

@@ -15,6 +15,11 @@ public interface CryptoComponent {
ErasableKey deriveMacKey(byte[] secret, boolean initiator);
byte[][] deriveInitialSecrets(byte[] theirPublicKey, KeyPair ourKeyPair,
int invitationCode, boolean initiator);
int deriveConfirmationCode(byte[] secret, boolean initiator);
byte[] deriveNextSecret(byte[] secret, int index, long connection);
KeyPair generateKeyPair();
@@ -25,6 +30,8 @@ public interface CryptoComponent {
MessageDigest getMessageDigest();
PseudoRandom getPseudoRandom(int seed);
SecureRandom getSecureRandom();
Cipher getTagCipher();

View File

@@ -0,0 +1,6 @@
package net.sf.briar.api.crypto;
public interface PseudoRandom {
byte[] nextBytes(int bytes);
}

View File

@@ -0,0 +1,6 @@
package net.sf.briar.api.plugins;
public interface IncomingInvitationCallback extends InvitationCallback {
int enterInvitationCode();
}

View File

@@ -0,0 +1,14 @@
package net.sf.briar.api.plugins;
public interface InvitationCallback {
boolean isCancelled();
int enterConfirmationCode(int code);
void showProgress(String... message);
void showFailure(String... message);
void showSuccess();
}

View File

@@ -0,0 +1,14 @@
package net.sf.briar.api.plugins;
public interface InvitationConstants {
static final long INVITATION_TIMEOUT = 60 * 1000; // 1 minute
static final int CODE_BITS = 19; // Codes must fit into six decimal digits
static final int MAX_CODE = 1 << CODE_BITS - 1;
static final int HASH_LENGTH = 48;
static final int MAX_PUBLIC_KEY_LENGTH = 120;
}

View File

@@ -0,0 +1,12 @@
package net.sf.briar.api.plugins;
import net.sf.briar.api.plugins.duplex.DuplexPlugin;
public interface InvitationStarter {
void startIncomingInvitation(DuplexPlugin plugin,
IncomingInvitationCallback callback);
void startOutgoingInvitation(DuplexPlugin plugin,
OutgoingInvitationCallback callback);
}

View File

@@ -0,0 +1,6 @@
package net.sf.briar.api.plugins;
public interface OutgoingInvitationCallback extends InvitationCallback {
void showInvitationCode(int code);
}

View File

@@ -1,5 +1,10 @@
package net.sf.briar.api.plugins;
import java.util.Collection;
import net.sf.briar.api.plugins.duplex.DuplexPlugin;
import net.sf.briar.api.plugins.simplex.SimplexPlugin;
public interface PluginManager {
/**
@@ -12,4 +17,10 @@ public interface PluginManager {
* Stops the plugins and returns the number of plugins successfully stopped.
*/
int stop();
/** Returns any duplex plugins that support invitations. */
Collection<DuplexPlugin> getDuplexInvitationPlugins();
/** Returns any simplex plugins that support invitations. */
Collection<SimplexPlugin> getSimplexInvitationPlugins();
}

View File

@@ -1,6 +1,7 @@
package net.sf.briar.api.plugins.duplex;
import net.sf.briar.api.ContactId;
import net.sf.briar.api.crypto.PseudoRandom;
import net.sf.briar.api.plugins.Plugin;
/** An interface for transport plugins that support duplex communication. */
@@ -17,11 +18,11 @@ public interface DuplexPlugin extends Plugin {
* Starts the invitation process from the inviter's side. Returns null if
* no connection can be established within the given timeout.
*/
DuplexTransportConnection sendInvitation(int code, long timeout);
DuplexTransportConnection sendInvitation(PseudoRandom r, long timeout);
/**
* Starts the invitation process from the invitee's side. Returns null if
* no connection can be established within the given timeout.
*/
DuplexTransportConnection acceptInvitation(int code, long timeout);
DuplexTransportConnection acceptInvitation(PseudoRandom r, long timeout);
}

View File

@@ -1,6 +1,7 @@
package net.sf.briar.api.plugins.simplex;
import net.sf.briar.api.ContactId;
import net.sf.briar.api.crypto.PseudoRandom;
import net.sf.briar.api.plugins.Plugin;
/** An interface for transport plugins that support simplex communication. */
@@ -24,23 +25,24 @@ public interface SimplexPlugin extends Plugin {
* Starts the invitation process from the inviter's side. Returns null if
* no connection can be established within the given timeout.
*/
SimplexTransportWriter sendInvitation(int code, long timeout);
SimplexTransportWriter sendInvitation(PseudoRandom r, long timeout);
/**
* Starts the invitation process from the invitee's side. Returns null if
* no connection can be established within the given timeout.
*/
SimplexTransportReader acceptInvitation(int code, long timeout);
SimplexTransportReader acceptInvitation(PseudoRandom r, long timeout);
/**
* Continues the invitation process from the invitee's side. Returns null
* if no connection can be established within the given timeout.
*/
SimplexTransportWriter sendInvitationResponse(int code, long timeout);
SimplexTransportWriter sendInvitationResponse(PseudoRandom r, long timeout);
/**
* Continues the invitation process from the inviter's side. Returns null
* if no connection can be established within the given timeout.
*/
SimplexTransportReader acceptInvitationResponse(int code, long timeout);
SimplexTransportReader acceptInvitationResponse(PseudoRandom r,
long timeout);
}

View File

@@ -1,13 +1,17 @@
package net.sf.briar.crypto;
import static net.sf.briar.api.plugins.InvitationConstants.CODE_BITS;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Security;
import java.security.Signature;
import javax.crypto.Cipher;
import javax.crypto.KeyAgreement;
import javax.crypto.Mac;
import javax.crypto.spec.IvParameterSpec;
@@ -15,6 +19,7 @@ import net.sf.briar.api.crypto.CryptoComponent;
import net.sf.briar.api.crypto.ErasableKey;
import net.sf.briar.api.crypto.KeyParser;
import net.sf.briar.api.crypto.MessageDigest;
import net.sf.briar.api.crypto.PseudoRandom;
import net.sf.briar.util.ByteUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
@@ -26,6 +31,7 @@ class CryptoComponentImpl implements CryptoComponent {
private static final String PROVIDER = "BC";
private static final String KEY_PAIR_ALGO = "ECDSA";
private static final int KEY_PAIR_BITS = 384;
private static final String KEY_AGREEMENT_ALGO = "ECDHC";
private static final String SECRET_KEY_ALGO = "AES";
private static final int SECRET_KEY_BYTES = 32; // 256 bits
private static final int KEY_DERIVATION_IV_BYTES = 16; // 128 bits
@@ -40,13 +46,17 @@ class CryptoComponentImpl implements CryptoComponent {
private static final byte[] TAG = { 'T', 'A', 'G', 0 };
private static final byte[] FRAME = { 'F', 'R', 'A', 'M', 'E', 0 };
private static final byte[] MAC = { 'M', 'A', 'C', 0 };
// Labels for secret derivation, null-terminated
private static final byte[] FIRST = { 'F', 'I', 'R', 'S', 'T', 0 };
private static final byte[] NEXT = { 'N', 'E', 'X', 'T', 0 };
// Context strings for key derivation
// Label for confirmation code derivation, null-terminated
private static final byte[] CODE = { 'C', 'O', 'D', 'E', 0 };
// Context strings for key and confirmation code derivation
private static final byte[] INITIATOR = { 'I' };
private static final byte[] RESPONDER = { 'R' };
// Blank plaintext for key derivation
private static final byte[] KEY_DERIVATION_INPUT =
new byte[SECRET_KEY_BYTES];
new byte[SECRET_KEY_BYTES];
private final KeyParser keyParser;
private final KeyPairGenerator keyPairGenerator;
@@ -114,6 +124,72 @@ class CryptoComponentImpl implements CryptoComponent {
}
}
public byte[][] deriveInitialSecrets(byte[] theirPublicKey,
KeyPair ourKeyPair, int invitationCode, boolean initiator) {
try {
PublicKey theirPublic = keyParser.parsePublicKey(theirPublicKey);
MessageDigest messageDigest = getMessageDigest();
byte[] ourPublicKey = ourKeyPair.getPublic().getEncoded();
byte[] ourHash = messageDigest.digest(ourPublicKey);
byte[] theirHash = messageDigest.digest(theirPublicKey);
// The initiator and responder info for the KDF are the hashes of
// the corresponding public keys
byte[] initiatorInfo, responderInfo;
if(initiator) {
initiatorInfo = ourHash;
responderInfo = theirHash;
} else {
initiatorInfo = theirHash;
responderInfo = ourHash;
}
// The public info for the KDF is the invitation code as a uint32
byte[] publicInfo = new byte[4];
ByteUtils.writeUint32(invitationCode, publicInfo, 0);
// The raw secret comes from the key agreement algorithm
KeyAgreement keyAgreement = KeyAgreement.getInstance(
KEY_AGREEMENT_ALGO, PROVIDER);
keyAgreement.init(ourKeyPair.getPrivate());
keyAgreement.doPhase(theirPublic, true);
byte[] rawSecret = keyAgreement.generateSecret();
// Derive the cooked secret from the raw secret
byte[] cookedSecret = concatenationKdf(rawSecret, FIRST,
initiatorInfo, responderInfo, publicInfo);
ByteUtils.erase(rawSecret);
// Derive the incoming and outgoing secrets from the cooked secret
byte[][] secrets = new byte[2][];
secrets[0] = counterModeKdf(cookedSecret, FIRST, INITIATOR);
secrets[1] = counterModeKdf(cookedSecret, FIRST, RESPONDER);
ByteUtils.erase(cookedSecret);
return secrets;
} catch(GeneralSecurityException e) {
return null;
}
}
// Key derivation function based on a hash function - see NIST SP 800-65A,
// section 5.8
private byte[] concatenationKdf(byte[] rawSecret, byte[] label,
byte[] initiatorInfo, byte[] responderInfo, byte[] publicInfo) {
// The output of the hash function must be long enough to use as a key
MessageDigest messageDigest = getMessageDigest();
if(messageDigest.getDigestLength() < SECRET_KEY_BYTES)
throw new RuntimeException();
byte[] rawSecretLength = new byte[4];
ByteUtils.writeUint32(rawSecret.length, rawSecretLength, 0);
messageDigest.update(rawSecretLength);
messageDigest.update(rawSecret);
messageDigest.update(label);
messageDigest.update(initiatorInfo);
messageDigest.update(responderInfo);
messageDigest.update(publicInfo);
byte[] hash = messageDigest.digest();
// The secret is the first SECRET_KEY_BYTES bytes of the hash
byte[] output = new byte[SECRET_KEY_BYTES];
System.arraycopy(hash, 0, output, 0, SECRET_KEY_BYTES);
ByteUtils.erase(hash);
return output;
}
public byte[] deriveNextSecret(byte[] secret, int index, long connection) {
if(index < 0 || index > ByteUtils.MAX_16_BIT_UNSIGNED)
throw new IllegalArgumentException();
@@ -125,6 +201,19 @@ class CryptoComponentImpl implements CryptoComponent {
return counterModeKdf(secret, NEXT, context);
}
public int deriveConfirmationCode(byte[] secret, boolean initiator) {
byte[] context = initiator ? INITIATOR : RESPONDER;
byte[] output = counterModeKdf(secret, CODE, context);
int code = extractCode(output);
ByteUtils.erase(output);
return code;
}
private int extractCode(byte[] secret) {
// Convert the first CODE_BITS bits of the secret into an unsigned int
return ByteUtils.readUint(secret, CODE_BITS);
}
public KeyPair generateKeyPair() {
return keyPairGenerator.generateKeyPair();
}
@@ -148,6 +237,10 @@ class CryptoComponentImpl implements CryptoComponent {
}
}
public PseudoRandom getPseudoRandom(int seed) {
return new PseudoRandomImpl(getMessageDigest(), seed);
}
public SecureRandom getSecureRandom() {
return secureRandom;
}

View File

@@ -0,0 +1,40 @@
package net.sf.briar.crypto;
import net.sf.briar.api.crypto.MessageDigest;
import net.sf.briar.api.crypto.PseudoRandom;
import net.sf.briar.util.ByteUtils;
class PseudoRandomImpl implements PseudoRandom {
private final MessageDigest messageDigest;
private byte[] state;
private int offset;
PseudoRandomImpl(MessageDigest messageDigest, int seed) {
this.messageDigest = messageDigest;
byte[] seedBytes = new byte[4];
ByteUtils.writeUint32(seed, seedBytes, 0);
messageDigest.update(seedBytes);
state = messageDigest.digest();
offset = 0;
}
public synchronized byte[] nextBytes(int bytes) {
byte[] b = new byte[bytes];
int half = state.length / 2;
int off = 0, len = b.length, available = half - offset;
while(available < len) {
System.arraycopy(state, offset, b, off, available);
off += available;
len -= available;
messageDigest.update(state, half, half);
state = messageDigest.digest();
offset = 0;
available = half;
}
System.arraycopy(state, offset, b, off, len);
offset += len;
return b;
}
}

View File

@@ -0,0 +1,225 @@
package net.sf.briar.plugins;
import static net.sf.briar.api.plugins.InvitationConstants.HASH_LENGTH;
import static net.sf.briar.api.plugins.InvitationConstants.INVITATION_TIMEOUT;
import static net.sf.briar.api.plugins.InvitationConstants.MAX_CODE;
import static net.sf.briar.api.plugins.InvitationConstants.MAX_PUBLIC_KEY_LENGTH;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.KeyPair;
import java.util.Arrays;
import java.util.concurrent.Executor;
import javax.inject.Inject;
import net.sf.briar.api.crypto.CryptoComponent;
import net.sf.briar.api.crypto.MessageDigest;
import net.sf.briar.api.crypto.PseudoRandom;
import net.sf.briar.api.db.DatabaseComponent;
import net.sf.briar.api.db.DbException;
import net.sf.briar.api.plugins.IncomingInvitationCallback;
import net.sf.briar.api.plugins.InvitationStarter;
import net.sf.briar.api.plugins.OutgoingInvitationCallback;
import net.sf.briar.api.plugins.PluginExecutor;
import net.sf.briar.api.plugins.duplex.DuplexPlugin;
import net.sf.briar.api.plugins.duplex.DuplexTransportConnection;
import net.sf.briar.api.serial.Reader;
import net.sf.briar.api.serial.ReaderFactory;
import net.sf.briar.api.serial.Writer;
import net.sf.briar.api.serial.WriterFactory;
import net.sf.briar.util.ByteUtils;
// FIXME: Refactor this class to remove duplicated code
class InvitationStarterImpl implements InvitationStarter {
private static final String TIMED_OUT = "INVITATION_TIMED_OUT";
private static final String IO_EXCEPTION = "INVITATION_IO_EXCEPTION";
private static final String INVALID_KEY = "INVITATION_INVALID_KEY";
private static final String WRONG_CODE = "INVITATION_WRONG_CODE";
private static final String DB_EXCEPTION = "INVITATION_DB_EXCEPTION";
private final Executor pluginExecutor;
private final CryptoComponent crypto;
private final DatabaseComponent db;
private final ReaderFactory readerFactory;
private final WriterFactory writerFactory;
@Inject
InvitationStarterImpl(@PluginExecutor Executor pluginExecutor,
CryptoComponent crypto, DatabaseComponent db,
ReaderFactory readerFactory, WriterFactory writerFactory) {
this.pluginExecutor = pluginExecutor;
this.crypto = crypto;
this.db = db;
this.readerFactory = readerFactory;
this.writerFactory = writerFactory;
}
public void startIncomingInvitation(final DuplexPlugin plugin,
final IncomingInvitationCallback callback) {
pluginExecutor.execute(new Runnable() {
public void run() {
long end = System.currentTimeMillis() + INVITATION_TIMEOUT;
// Get the invitation code from the inviter
int code = callback.enterInvitationCode();
if(code == -1) return;
long remaining = end - System.currentTimeMillis();
if(remaining <= 0) return;
// Use the invitation code to seed the PRNG
PseudoRandom r = crypto.getPseudoRandom(code);
// Connect to the inviter
DuplexTransportConnection conn = plugin.acceptInvitation(r,
remaining);
if(callback.isCancelled()) {
if(conn != null) conn.dispose(false, false);
return;
}
if(conn == null) {
callback.showFailure(TIMED_OUT);
return;
}
KeyPair ourKeyPair = crypto.generateKeyPair();
MessageDigest messageDigest = crypto.getMessageDigest();
byte[] ourKey = ourKeyPair.getPublic().getEncoded();
byte[] ourHash = messageDigest.digest(ourKey);
byte[] theirKey, theirHash;
try {
// Send the public key hash
OutputStream out = conn.getOutputStream();
Writer writer = writerFactory.createWriter(out);
writer.writeBytes(ourHash);
out.flush();
// Receive the public key hash
InputStream in = conn.getInputStream();
Reader reader = readerFactory.createReader(in);
theirHash = reader.readBytes(HASH_LENGTH);
// Send the public key
writer.writeBytes(ourKey);
out.flush();
// Receive the public key
theirKey = reader.readBytes(MAX_PUBLIC_KEY_LENGTH);
} catch(IOException e) {
conn.dispose(true, false);
callback.showFailure(IO_EXCEPTION);
return;
}
conn.dispose(false, false);
if(callback.isCancelled()) return;
// Check that the received hash matches the received key
if(!Arrays.equals(theirHash, messageDigest.digest(theirKey))) {
callback.showFailure(INVALID_KEY);
return;
}
// Derive the initial shared secrets and the confirmation codes
byte[][] secrets = crypto.deriveInitialSecrets(theirKey,
ourKeyPair, code, false);
if(secrets == null) {
callback.showFailure(INVALID_KEY);
return;
}
int theirCode = crypto.deriveConfirmationCode(secrets[0], true);
int ourCode = crypto.deriveConfirmationCode(secrets[1], false);
// Compare the confirmation codes
if(callback.enterConfirmationCode(ourCode) != theirCode) {
callback.showFailure(WRONG_CODE);
ByteUtils.erase(secrets[0]);
ByteUtils.erase(secrets[1]);
return;
}
// Add the contact to the database
try {
db.addContact(secrets[0], secrets[1]);
} catch(DbException e) {
callback.showFailure(DB_EXCEPTION);
ByteUtils.erase(secrets[0]);
ByteUtils.erase(secrets[1]);
return;
}
callback.showSuccess();
}
});
}
public void startOutgoingInvitation(final DuplexPlugin plugin,
final OutgoingInvitationCallback callback) {
pluginExecutor.execute(new Runnable() {
public void run() {
// Generate an invitation code and use it to seed the PRNG
int code = crypto.getSecureRandom().nextInt(MAX_CODE + 1);
PseudoRandom r = crypto.getPseudoRandom(code);
// Connect to the invitee
DuplexTransportConnection conn = plugin.sendInvitation(r,
INVITATION_TIMEOUT);
if(callback.isCancelled()) {
if(conn != null) conn.dispose(false, false);
return;
}
if(conn == null) {
callback.showFailure(TIMED_OUT);
return;
}
KeyPair ourKeyPair = crypto.generateKeyPair();
MessageDigest messageDigest = crypto.getMessageDigest();
byte[] ourKey = ourKeyPair.getPublic().getEncoded();
byte[] ourHash = messageDigest.digest(ourKey);
byte[] theirKey, theirHash;
try {
// Receive the public key hash
InputStream in = conn.getInputStream();
Reader reader = readerFactory.createReader(in);
theirHash = reader.readBytes(HASH_LENGTH);
// Send the public key hash
OutputStream out = conn.getOutputStream();
Writer writer = writerFactory.createWriter(out);
writer.writeBytes(ourHash);
out.flush();
// Receive the public key
theirKey = reader.readBytes(MAX_PUBLIC_KEY_LENGTH);
// Send the public key
writer.writeBytes(ourKey);
out.flush();
} catch(IOException e) {
conn.dispose(true, false);
callback.showFailure(IO_EXCEPTION);
return;
}
conn.dispose(false, false);
if(callback.isCancelled()) return;
// Check that the received hash matches the received key
if(!Arrays.equals(theirHash, messageDigest.digest(theirKey))) {
callback.showFailure(INVALID_KEY);
return;
}
// Derive the shared secret and the confirmation codes
byte[][] secrets = crypto.deriveInitialSecrets(theirKey,
ourKeyPair, code, true);
if(secrets == null) {
callback.showFailure(INVALID_KEY);
return;
}
int ourCode = crypto.deriveConfirmationCode(secrets[0], true);
int theirCode = crypto.deriveConfirmationCode(secrets[1],
false);
// Compare the confirmation codes
if(callback.enterConfirmationCode(ourCode) != theirCode) {
callback.showFailure(WRONG_CODE);
ByteUtils.erase(secrets[0]);
ByteUtils.erase(secrets[1]);
return;
}
// Add the contact to the database
try {
db.addContact(secrets[1], secrets[0]);
} catch(DbException e) {
callback.showFailure(DB_EXCEPTION);
ByteUtils.erase(secrets[0]);
ByteUtils.erase(secrets[1]);
return;
}
callback.showSuccess();
}
});
}
}

View File

@@ -2,6 +2,7 @@ package net.sf.briar.plugins;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
@@ -40,7 +41,7 @@ import com.google.inject.Inject;
class PluginManagerImpl implements PluginManager {
private static final Logger LOG =
Logger.getLogger(PluginManagerImpl.class.getName());
Logger.getLogger(PluginManagerImpl.class.getName());
private static final String[] SIMPLEX_PLUGIN_FACTORIES = new String[] {
"net.sf.briar.plugins.file.RemovableDrivePluginFactory"
@@ -84,7 +85,7 @@ class PluginManagerImpl implements PluginManager {
try {
Class<?> c = Class.forName(s);
SimplexPluginFactory factory =
(SimplexPluginFactory) c.newInstance();
(SimplexPluginFactory) c.newInstance();
SimplexCallback callback = new SimplexCallback();
SimplexPlugin plugin = factory.createPlugin(pluginExecutor,
callback);
@@ -124,7 +125,7 @@ class PluginManagerImpl implements PluginManager {
try {
Class<?> c = Class.forName(s);
DuplexPluginFactory factory =
(DuplexPluginFactory) c.newInstance();
(DuplexPluginFactory) c.newInstance();
DuplexCallback callback = new DuplexCallback();
DuplexPlugin plugin = factory.createPlugin(pluginExecutor,
callback);
@@ -198,6 +199,26 @@ class PluginManagerImpl implements PluginManager {
return stopped;
}
public Collection<DuplexPlugin> getDuplexInvitationPlugins() {
Collection<DuplexPlugin> supported = new ArrayList<DuplexPlugin>();
synchronized(this) {
for(DuplexPlugin d : duplexPlugins) {
if(d.supportsInvitations()) supported.add(d);
}
}
return supported;
}
public Collection<SimplexPlugin> getSimplexInvitationPlugins() {
Collection<SimplexPlugin> supported = new ArrayList<SimplexPlugin>();
synchronized(this) {
for(SimplexPlugin s : simplexPlugins) {
if(s.supportsInvitations()) supported.add(s);
}
}
return supported;
}
private abstract class PluginCallbackImpl implements PluginCallback {
protected volatile TransportId id = null;

View File

@@ -25,6 +25,7 @@ import javax.microedition.io.StreamConnectionNotifier;
import net.sf.briar.api.ContactId;
import net.sf.briar.api.TransportProperties;
import net.sf.briar.api.crypto.PseudoRandom;
import net.sf.briar.api.plugins.PluginExecutor;
import net.sf.briar.api.plugins.duplex.DuplexPlugin;
import net.sf.briar.api.plugins.duplex.DuplexPluginCallback;
@@ -304,22 +305,25 @@ class BluetoothPlugin implements DuplexPlugin {
return true;
}
public DuplexTransportConnection sendInvitation(int code, long timeout) {
return createInvitationConnection(code, timeout);
public DuplexTransportConnection sendInvitation(PseudoRandom r,
long timeout) {
return createInvitationConnection(r, timeout);
}
public DuplexTransportConnection acceptInvitation(int code, long timeout) {
return createInvitationConnection(code, timeout);
public DuplexTransportConnection acceptInvitation(PseudoRandom r,
long timeout) {
return createInvitationConnection(r, timeout);
}
private DuplexTransportConnection createInvitationConnection(int code,
private DuplexTransportConnection createInvitationConnection(PseudoRandom r,
long timeout) {
synchronized(this) {
if(!running) return null;
}
// Use the invitation code to generate the UUID
String uuid = StringUtils.toHexString(r.nextBytes(16));
// The invitee's device may not be discoverable, so both parties must
// try to initiate connections
String uuid = convertInvitationCodeToUuid(code);
final ConnectionCallback c = new ConnectionCallback(uuid, timeout);
pluginExecutor.execute(new Runnable() {
public void run() {
@@ -342,12 +346,6 @@ class BluetoothPlugin implements DuplexPlugin {
}
}
private String convertInvitationCodeToUuid(int code) {
byte[] b = new byte[16];
new Random(code).nextBytes(b);
return StringUtils.toHexString(b);
}
private void createInvitationConnection(ConnectionCallback c) {
LocalDevice localDevice;
synchronized(this) {

View File

@@ -11,19 +11,21 @@ import java.util.logging.Level;
import java.util.logging.Logger;
import net.sf.briar.api.ContactId;
import net.sf.briar.api.crypto.PseudoRandom;
import net.sf.briar.api.plugins.PluginExecutor;
import net.sf.briar.api.plugins.simplex.SimplexPlugin;
import net.sf.briar.api.plugins.simplex.SimplexPluginCallback;
import net.sf.briar.api.plugins.simplex.SimplexTransportReader;
import net.sf.briar.api.plugins.simplex.SimplexTransportWriter;
import net.sf.briar.api.transport.TransportConstants;
import net.sf.briar.util.StringUtils;
import org.apache.commons.io.FileSystemUtils;
abstract class FilePlugin implements SimplexPlugin {
private static final Logger LOG =
Logger.getLogger(FilePlugin.class.getName());
Logger.getLogger(FilePlugin.class.getName());
protected final Executor pluginExecutor;
protected final SimplexPluginCallback callback;
@@ -92,26 +94,28 @@ abstract class FilePlugin implements SimplexPlugin {
pluginExecutor.execute(new ReaderCreator(f));
}
public SimplexTransportWriter sendInvitation(int code, long timeout) {
public SimplexTransportWriter sendInvitation(PseudoRandom r, long timeout) {
if(!running) return null;
return createWriter(createInvitationFilename(code, false));
return createWriter(createInvitationFilename(r, false));
}
public SimplexTransportReader acceptInvitation(int code, long timeout) {
public SimplexTransportReader acceptInvitation(PseudoRandom r,
long timeout) {
if(!running) return null;
String filename = createInvitationFilename(code, false);
String filename = createInvitationFilename(r, false);
return createInvitationReader(filename, timeout);
}
public SimplexTransportWriter sendInvitationResponse(int code, long timeout) {
if(!running) return null;
return createWriter(createInvitationFilename(code, true));
}
public SimplexTransportReader acceptInvitationResponse(int code,
public SimplexTransportWriter sendInvitationResponse(PseudoRandom r,
long timeout) {
if(!running) return null;
String filename = createInvitationFilename(code, true);
return createWriter(createInvitationFilename(r, true));
}
public SimplexTransportReader acceptInvitationResponse(PseudoRandom r,
long timeout) {
if(!running) return null;
String filename = createInvitationFilename(r, true);
return createInvitationReader(filename, timeout);
}
@@ -149,15 +153,14 @@ abstract class FilePlugin implements SimplexPlugin {
return null;
}
private String createInvitationFilename(int code, boolean response) {
assert code >= 0;
assert code < 10 * 1000 * 1000;
return String.format("%c%7d.dat", response ? 'b' : 'a', code);
private String createInvitationFilename(PseudoRandom r, boolean response) {
String digits = StringUtils.toHexString(r.nextBytes(3));
return String.format("%c%s.dat", response ? 'b' : 'a', digits);
}
// Package access for testing
boolean isPossibleInvitationFilename(String filename) {
return filename.toLowerCase().matches("[ab][0-9]{7}.dat");
return filename.toLowerCase().matches("[ab][0-9a-f]{6}.dat");
}
private class ReaderCreator implements Runnable {

View File

@@ -9,11 +9,11 @@ import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.Random;
import java.util.concurrent.Executor;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.sf.briar.api.crypto.PseudoRandom;
import net.sf.briar.api.plugins.PluginExecutor;
import net.sf.briar.api.plugins.duplex.DuplexPluginCallback;
import net.sf.briar.api.plugins.duplex.DuplexTransportConnection;
@@ -23,7 +23,7 @@ import net.sf.briar.util.ByteUtils;
class LanSocketPlugin extends SimpleSocketPlugin {
private static final Logger LOG =
Logger.getLogger(LanSocketPlugin.class.getName());
Logger.getLogger(LanSocketPlugin.class.getName());
LanSocketPlugin(@PluginExecutor Executor pluginExecutor,
DuplexPluginCallback callback, long pollingInterval) {
@@ -36,12 +36,13 @@ class LanSocketPlugin extends SimpleSocketPlugin {
}
@Override
public DuplexTransportConnection sendInvitation(int code, long timeout) {
public DuplexTransportConnection sendInvitation(PseudoRandom r,
long timeout) {
synchronized(this) {
if(!running) return null;
}
// Calculate the group address and port from the invitation code
InetSocketAddress mcast = convertInvitationCodeToMulticastGroup(code);
// Use the invitation code to choose the group address and port
InetSocketAddress mcast = chooseMulticastGroup(r);
// Bind a multicast socket for receiving packets
MulticastSocket ms = null;
try {
@@ -105,10 +106,8 @@ class LanSocketPlugin extends SimpleSocketPlugin {
ms.close();
}
private InetSocketAddress convertInvitationCodeToMulticastGroup(int code) {
Random r = new Random(code);
byte[] b = new byte[5];
r.nextBytes(b);
private InetSocketAddress chooseMulticastGroup(PseudoRandom r) {
byte[] b = r.nextBytes(5);
// The group address is 239.random.random.random, excluding 0 and 255
byte[] group = new byte[4];
group[0] = (byte) 239;
@@ -139,12 +138,13 @@ class LanSocketPlugin extends SimpleSocketPlugin {
}
@Override
public DuplexTransportConnection acceptInvitation(int code, long timeout) {
public DuplexTransportConnection acceptInvitation(PseudoRandom r,
long timeout) {
synchronized(this) {
if(!running) return null;
}
// Calculate the group address and port from the invitation code
InetSocketAddress mcast = convertInvitationCodeToMulticastGroup(code);
// Use the invitation code to choose the group address and port
InetSocketAddress mcast = chooseMulticastGroup(r);
// Bind a TCP socket for receiving connections
ServerSocket ss = null;
try {

View File

@@ -15,6 +15,7 @@ import java.util.logging.Logger;
import net.sf.briar.api.ContactId;
import net.sf.briar.api.TransportProperties;
import net.sf.briar.api.crypto.PseudoRandom;
import net.sf.briar.api.plugins.PluginExecutor;
import net.sf.briar.api.plugins.duplex.DuplexPluginCallback;
import net.sf.briar.api.plugins.duplex.DuplexTransportConnection;
@@ -24,13 +25,13 @@ import net.sf.briar.util.StringUtils;
class SimpleSocketPlugin extends SocketPlugin {
public static final byte[] TRANSPORT_ID =
StringUtils.fromHexString("58c66d999e492b85065924acfd739d80"
+ "c65a62f87e5a4fc6c284f95908b9007d"
+ "512a93ebf89bf68f50a29e96eebf97b6");
StringUtils.fromHexString("58c66d999e492b85065924acfd739d80"
+ "c65a62f87e5a4fc6c284f95908b9007d"
+ "512a93ebf89bf68f50a29e96eebf97b6");
private static final TransportId ID = new TransportId(TRANSPORT_ID);
private static final Logger LOG =
Logger.getLogger(SimpleSocketPlugin.class.getName());
Logger.getLogger(SimpleSocketPlugin.class.getName());
SimpleSocketPlugin(@PluginExecutor Executor pluginExecutor,
DuplexPluginCallback callback, long pollingInterval) {
@@ -68,7 +69,7 @@ class SimpleSocketPlugin extends SocketPlugin {
protected InetAddress chooseInterface(boolean lan) throws IOException {
List<NetworkInterface> ifaces =
Collections.list(NetworkInterface.getNetworkInterfaces());
Collections.list(NetworkInterface.getNetworkInterfaces());
// Try to find an interface of the preferred type (LAN or WAN)
for(NetworkInterface iface : ifaces) {
for(InetAddress addr : Collections.list(iface.getInetAddresses())) {
@@ -139,11 +140,13 @@ class SimpleSocketPlugin extends SocketPlugin {
return false;
}
public DuplexTransportConnection sendInvitation(int code, long timeout) {
public DuplexTransportConnection sendInvitation(PseudoRandom r,
long timeout) {
throw new UnsupportedOperationException();
}
public DuplexTransportConnection acceptInvitation(int code, long timeout) {
public DuplexTransportConnection acceptInvitation(PseudoRandom r,
long timeout) {
throw new UnsupportedOperationException();
}
}

View File

@@ -10,6 +10,7 @@ import java.util.logging.Logger;
import net.sf.briar.api.ContactId;
import net.sf.briar.api.TransportConfig;
import net.sf.briar.api.TransportProperties;
import net.sf.briar.api.crypto.PseudoRandom;
import net.sf.briar.api.plugins.PluginExecutor;
import net.sf.briar.api.plugins.duplex.DuplexPlugin;
import net.sf.briar.api.plugins.duplex.DuplexPluginCallback;
@@ -33,13 +34,13 @@ import org.silvertunnel.netlib.layer.tor.util.RSAKeyPair;
class TorPlugin implements DuplexPlugin {
public static final byte[] TRANSPORT_ID =
StringUtils.fromHexString("f264721575cb7ee710772f35abeb3db4"
+ "a91f474e14de346be296c2efc99effdd"
+ "f35921e6ed87a25c201f044da4767981");
StringUtils.fromHexString("f264721575cb7ee710772f35abeb3db4"
+ "a91f474e14de346be296c2efc99effdd"
+ "f35921e6ed87a25c201f044da4767981");
private static final TransportId ID = new TransportId(TRANSPORT_ID);
private static final Logger LOG =
Logger.getLogger(TorPlugin.class.getName());
Logger.getLogger(TorPlugin.class.getName());
private final Executor pluginExecutor;
private final DuplexPluginCallback callback;
@@ -89,7 +90,7 @@ class TorPlugin implements DuplexPlugin {
}
}
TorHiddenServicePortPrivateNetAddress addrPort =
new TorHiddenServicePortPrivateNetAddress(addr, 80);
new TorHiddenServicePortPrivateNetAddress(addr, 80);
// Connect to Tor
NetFactory netFactory = NetFactory.getInstance();
NetLayer nl = netFactory.getNetLayerById(NetLayerIDs.TOR);
@@ -129,7 +130,7 @@ class TorPlugin implements DuplexPlugin {
private TorHiddenServicePrivateNetAddress createHiddenServiceAddress(
TorNetLayerUtil util, TransportConfig c) {
TorHiddenServicePrivateNetAddress addr =
util.createNewTorHiddenServicePrivateNetAddress();
util.createNewTorHiddenServicePrivateNetAddress();
RSAKeyPair keyPair = addr.getKeyPair();
String privateKey = Encryption.getPEMStringFromRSAKeyPair(keyPair);
c.put("privateKey", privateKey);
@@ -199,7 +200,7 @@ class TorPlugin implements DuplexPlugin {
if(!running) return;
}
Map<ContactId, TransportProperties> remote =
callback.getRemoteProperties();
callback.getRemoteProperties();
for(final ContactId c : remote.keySet()) {
if(connected.contains(c)) continue;
pluginExecutor.execute(new Runnable() {
@@ -249,11 +250,13 @@ class TorPlugin implements DuplexPlugin {
}
}
public DuplexTransportConnection sendInvitation(int code, long timeout) {
public DuplexTransportConnection sendInvitation(PseudoRandom r,
long timeout) {
throw new UnsupportedOperationException();
}
public DuplexTransportConnection acceptInvitation(int code, long timeout) {
public DuplexTransportConnection acceptInvitation(PseudoRandom r,
long timeout) {
throw new UnsupportedOperationException();
}
}

View File

@@ -29,7 +29,7 @@ public abstract class DuplexClientTest extends DuplexTest {
}
// Try to send an invitation
System.out.println("Sending invitation");
d = plugin.sendInvitation(123, INVITATION_TIMEOUT);
d = plugin.sendInvitation(getPseudoRandom(123), INVITATION_TIMEOUT);
if(d == null) {
System.out.println("Connection failed");
} else {
@@ -38,7 +38,7 @@ public abstract class DuplexClientTest extends DuplexTest {
}
// Try to accept an invitation
System.out.println("Accepting invitation");
d = plugin.acceptInvitation(456, INVITATION_TIMEOUT);
d = plugin.acceptInvitation(getPseudoRandom(456), INVITATION_TIMEOUT);
if(d == null) {
System.out.println("Connection failed");
} else {

View File

@@ -24,8 +24,8 @@ public abstract class DuplexServerTest extends DuplexTest {
callback.latch.await();
// Try to accept an invitation
System.out.println("Accepting invitation");
DuplexTransportConnection d = plugin.acceptInvitation(123,
INVITATION_TIMEOUT);
DuplexTransportConnection d = plugin.acceptInvitation(
getPseudoRandom(123), INVITATION_TIMEOUT);
if(d == null) {
System.out.println("Connection failed");
} else {
@@ -34,7 +34,7 @@ public abstract class DuplexServerTest extends DuplexTest {
}
// Try to send an invitation
System.out.println("Sending invitation");
d = plugin.sendInvitation(456, INVITATION_TIMEOUT);
d = plugin.sendInvitation(getPseudoRandom(456), INVITATION_TIMEOUT);
if(d == null) {
System.out.println("Connection failed");
} else {

View File

@@ -2,9 +2,11 @@ package net.sf.briar.plugins;
import java.io.IOException;
import java.io.PrintStream;
import java.util.Random;
import java.util.Scanner;
import net.sf.briar.api.ContactId;
import net.sf.briar.api.crypto.PseudoRandom;
import net.sf.briar.api.plugins.duplex.DuplexPlugin;
import net.sf.briar.api.plugins.duplex.DuplexTransportConnection;
@@ -66,4 +68,23 @@ abstract class DuplexTest {
d.dispose(true, true);
}
}
protected PseudoRandom getPseudoRandom(int seed) {
return new TestPseudoRandom(seed);
}
private static class TestPseudoRandom implements PseudoRandom {
private final Random r;
private TestPseudoRandom(int seed) {
r = new Random(seed);
}
public byte[] nextBytes(int bytes) {
byte[] b = new byte[bytes];
r.nextBytes(b);
return b;
}
}
}

View File

@@ -48,4 +48,19 @@ public class ByteUtilsTest extends BriarTestCase {
ByteUtils.writeUint32(4294967295L, b, 1);
assertEquals("00FFFFFFFF", StringUtils.toHexString(b));
}
@Test
public void testReadUint() {
byte[] b = new byte[1];
b[0] = (byte) 128;
for(int i = 0; i < 8; i++) {
assertEquals(1 << i, ByteUtils.readUint(b, i + 1));
}
b = new byte[2];
for(int i = 0; i < 65535; i++) {
ByteUtils.writeUint16(i, b, 0);
assertEquals(i, ByteUtils.readUint(b, 16));
assertEquals(i >> 1, ByteUtils.readUint(b, 15));
}
}
}

View File

@@ -38,10 +38,21 @@ public class ByteUtils {
public static long readUint32(byte[] b, int offset) {
if(b.length < offset + 4) throw new IllegalArgumentException();
return ((b[offset] & 0xFFL) << 24) | ((b[offset + 1] & 0xFFL) << 16)
| ((b[offset + 2] & 0xFFL) << 8) | (b[offset + 3] & 0xFFL);
| ((b[offset + 2] & 0xFFL) << 8) | (b[offset + 3] & 0xFFL);
}
public static void erase(byte[] b) {
for(int i = 0; i < b.length; i++) b[i] = 0;
}
public static int readUint(byte[] b, int bits) {
if(b.length << 3 < bits) throw new IllegalArgumentException();
int result = 0;
for(int i = 0; i < bits; i++) {
if((b[i >> 3] & 128 >> (i & 7)) != 0) result |= 1 << bits - i - 1;
}
assert result >= 0;
assert result < 1 << bits;
return result;
}
}