A real working implementation of the invitation protocol.

This commit is contained in:
akwizgran
2012-11-13 12:26:33 +00:00
parent 54ca7decbf
commit f69f6b3d43
10 changed files with 314 additions and 222 deletions

View File

@@ -1,7 +1,7 @@
package net.sf.briar.api.crypto;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.SecureRandom;
import java.security.Signature;
@@ -32,8 +32,8 @@ public interface CryptoComponent {
* corresponding private keys.
* @param alice indicates whether the private key belongs to Alice or Bob.
*/
byte[] deriveInitialSecret(byte[] ourPublicKey, byte[] theirPublicKey,
PrivateKey ourPrivateKey, boolean alice);
byte[] deriveInitialSecret(byte[] theirPublicKey, KeyPair ourKeyPair,
boolean alice) throws GeneralSecurityException;
/**
* Generates a random invitation code.

View File

@@ -2,11 +2,11 @@ package net.sf.briar.api.plugins;
public interface InvitationConstants {
long INVITATION_TIMEOUT = 60 * 1000; // 1 minute
long INVITATION_TIMEOUT = 30 * 1000; // Milliseconds
int CODE_BITS = 19; // Codes must fit into six decimal digits
int MAX_CODE = (1 << CODE_BITS) - 1;
int MAX_CODE = (1 << CODE_BITS) - 1; // 524287
int HASH_LENGTH = 48; // Bytes

View File

@@ -6,6 +6,9 @@ import java.util.Map;
public interface Writer {
void flush() throws IOException;
void close() throws IOException;
void addConsumer(Consumer c);
void removeConsumer(Consumer c);

View File

@@ -8,7 +8,6 @@ import static net.sf.briar.util.ByteUtils.MAX_32_BIT_UNSIGNED;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Security;
@@ -142,38 +141,34 @@ class CryptoComponentImpl implements CryptoComponent {
}
}
public byte[] deriveInitialSecret(byte[] ourPublicKey,
byte[] theirPublicKey, PrivateKey ourPrivateKey, boolean alice) {
try {
PublicKey theirPublic = agreementKeyParser.parsePublicKey(
theirPublicKey);
MessageDigest messageDigest = getMessageDigest();
byte[] ourHash = messageDigest.digest(ourPublicKey);
byte[] theirHash = messageDigest.digest(theirPublicKey);
byte[] aliceInfo, bobInfo;
if(alice) {
aliceInfo = ourHash;
bobInfo = theirHash;
} else {
aliceInfo = theirHash;
bobInfo = ourHash;
}
// The raw secret comes from the key agreement algorithm
KeyAgreement keyAgreement = KeyAgreement.getInstance(AGREEMENT_ALGO,
PROVIDER);
keyAgreement.init(ourPrivateKey);
keyAgreement.doPhase(theirPublic, true);
byte[] rawSecret = keyAgreement.generateSecret();
// Derive the cooked secret from the raw secret using the
// concatenation KDF
byte[] cookedSecret = concatenationKdf(rawSecret, FIRST, aliceInfo,
bobInfo);
ByteUtils.erase(rawSecret);
return cookedSecret;
} catch(GeneralSecurityException e) {
// FIXME: Throw instead of returning null?
return null;
public byte[] deriveInitialSecret(byte[] theirPublicKey,
KeyPair ourKeyPair, boolean alice) throws GeneralSecurityException {
PublicKey theirPublic = agreementKeyParser.parsePublicKey(
theirPublicKey);
MessageDigest messageDigest = getMessageDigest();
byte[] ourPublicKey = ourKeyPair.getPublic().getEncoded();
byte[] ourHash = messageDigest.digest(ourPublicKey);
byte[] theirHash = messageDigest.digest(theirPublicKey);
byte[] aliceInfo, bobInfo;
if(alice) {
aliceInfo = ourHash;
bobInfo = theirHash;
} else {
aliceInfo = theirHash;
bobInfo = ourHash;
}
// The raw secret comes from the key agreement algorithm
KeyAgreement keyAgreement = KeyAgreement.getInstance(AGREEMENT_ALGO,
PROVIDER);
keyAgreement.init(ourKeyPair.getPrivate());
keyAgreement.doPhase(theirPublic, true);
byte[] rawSecret = keyAgreement.generateSecret();
// Derive the cooked secret from the raw secret using the
// concatenation KDF
byte[] cookedSecret = concatenationKdf(rawSecret, FIRST, aliceInfo,
bobInfo);
ByteUtils.erase(rawSecret);
return cookedSecret;
}
// Key derivation function based on a hash function - see NIST SP 800-56A,

View File

@@ -2,48 +2,47 @@ package net.sf.briar.invitation;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static net.sf.briar.api.plugins.InvitationConstants.HASH_LENGTH;
import static net.sf.briar.api.plugins.InvitationConstants.INVITATION_TIMEOUT;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.GeneralSecurityException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Logger;
import net.sf.briar.api.crypto.CryptoComponent;
import net.sf.briar.api.crypto.PseudoRandom;
import net.sf.briar.api.invitation.ConnectionCallback;
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;
class AliceConnector extends Thread {
class AliceConnector extends Connector {
private static final Logger LOG =
Logger.getLogger(AliceConnector.class.getName());
private final DuplexPlugin plugin;
private final PseudoRandom random;
private final ConnectionCallback callback;
private final AtomicBoolean connected, succeeded;
private final String pluginName;
AliceConnector(DuplexPlugin plugin, PseudoRandom random,
ConnectionCallback callback, AtomicBoolean connected,
AtomicBoolean succeeded) {
this.plugin = plugin;
this.random = random;
this.callback = callback;
this.connected = connected;
this.succeeded = succeeded;
pluginName = plugin.getClass().getName();
AliceConnector(CryptoComponent crypto, ReaderFactory readerFactory,
WriterFactory writerFactory, DuplexPlugin plugin,
PseudoRandom random, ConnectionCallback callback,
AtomicBoolean connected, AtomicBoolean succeeded) {
super(crypto, readerFactory, writerFactory, plugin, random, callback,
connected, succeeded);
}
@Override
public void run() {
// Try an outgoing connection first, then an incoming connection
long halfTime = System.currentTimeMillis() + INVITATION_TIMEOUT / 2;
DuplexTransportConnection conn = makeOutgoingConnection();
if(conn == null) conn = acceptIncomingConnection(halfTime);
if(conn == null) {
waitForHalfTime(halfTime);
conn = acceptIncomingConnection();
}
if(conn == null) return;
if(LOG.isLoggable(INFO)) LOG.info(pluginName + " connected");
// Don't proceed with more than one connection
@@ -52,34 +51,42 @@ class AliceConnector extends Thread {
tryToClose(conn, false);
return;
}
// FIXME: Carry out the real invitation protocol
// Carry out the key agreement protocol
InputStream in;
OutputStream out;
Reader r;
Writer w;
byte[] secret;
try {
in = conn.getInputStream();
OutputStream out = conn.getOutputStream();
byte[] hash = random.nextBytes(HASH_LENGTH);
out.write(hash);
out.flush();
if(LOG.isLoggable(INFO)) LOG.info(pluginName + " sent hash");
int offset = 0;
while(offset < hash.length) {
int read = in.read(hash, offset, hash.length - offset);
if(read == -1) break;
offset += read;
}
if(offset < HASH_LENGTH) throw new EOFException();
if(LOG.isLoggable(INFO)) LOG.info(pluginName + " received hash");
if(LOG.isLoggable(INFO)) LOG.info(pluginName + " succeeded");
succeeded.set(true);
callback.connectionEstablished(123456, 123456,
new ConfirmationSender(out));
out = conn.getOutputStream();
r = readerFactory.createReader(in);
w = writerFactory.createWriter(out);
// Alice goes first
sendPublicKeyHash(w);
byte[] hash = receivePublicKeyHash(r);
sendPublicKey(w);
byte[] key = receivePublicKey(r);
secret = deriveSharedSecret(hash, key, true);
} catch(IOException e) {
if(LOG.isLoggable(WARNING)) LOG.warning(e.toString());
tryToClose(conn, true);
return;
} catch(GeneralSecurityException e) {
if(LOG.isLoggable(WARNING)) LOG.warning(e.toString());
tryToClose(conn, true);
return;
}
// The key agreement succeeded
if(LOG.isLoggable(INFO)) LOG.info(pluginName + " succeeded");
succeeded.set(true);
// Derive the confirmation codes
int[] codes = crypto.deriveConfirmationCodes(secret);
callback.connectionEstablished(codes[0], codes[1],
new ConfirmationSender(w));
// Check whether the remote peer's confirmation codes matched
try {
if(in.read() == 1) callback.codesMatch();
if(r.readBoolean()) callback.codesMatch();
else callback.codesDoNotMatch();
} catch(IOException e) {
if(LOG.isLoggable(WARNING)) LOG.warning(e.toString());
@@ -87,35 +94,4 @@ class AliceConnector extends Thread {
callback.codesDoNotMatch();
}
}
private DuplexTransportConnection makeOutgoingConnection() {
if(LOG.isLoggable(INFO))
LOG.info(pluginName + " making outgoing connection");
return plugin.sendInvitation(random, INVITATION_TIMEOUT / 2);
}
private DuplexTransportConnection acceptIncomingConnection(long halfTime) {
long now = System.currentTimeMillis();
if(now < halfTime) {
if(LOG.isLoggable(INFO))
LOG.info(pluginName + " sleeping until half-time");
try {
Thread.sleep(halfTime - now);
} catch(InterruptedException e) {
if(LOG.isLoggable(INFO)) LOG.info("Interrupted while sleeping");
return null;
}
}
if(LOG.isLoggable(INFO))
LOG.info(pluginName + " accepting incoming connection");
return plugin.acceptInvitation(random, INVITATION_TIMEOUT / 2);
}
private void tryToClose(DuplexTransportConnection conn, boolean exception) {
try {
conn.dispose(exception, true);
} catch(IOException e) {
if(LOG.isLoggable(WARNING)) LOG.warning(e.toString());
}
}
}

View File

@@ -2,84 +2,85 @@ package net.sf.briar.invitation;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static net.sf.briar.api.plugins.InvitationConstants.HASH_LENGTH;
import static net.sf.briar.api.plugins.InvitationConstants.INVITATION_TIMEOUT;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.GeneralSecurityException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Logger;
import net.sf.briar.api.crypto.CryptoComponent;
import net.sf.briar.api.crypto.PseudoRandom;
import net.sf.briar.api.invitation.ConnectionCallback;
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;
class BobConnector extends Thread {
class BobConnector extends Connector {
private static final Logger LOG =
Logger.getLogger(BobConnector.class.getName());
private final DuplexPlugin plugin;
private final PseudoRandom random;
private final ConnectionCallback callback;
private final AtomicBoolean connected, succeeded;
private final String pluginName;
BobConnector(DuplexPlugin plugin, PseudoRandom random,
ConnectionCallback callback, AtomicBoolean connected,
AtomicBoolean succeeded) {
this.plugin = plugin;
this.random = random;
this.callback = callback;
this.connected = connected;
this.succeeded = succeeded;
pluginName = plugin.getClass().getName();
BobConnector(CryptoComponent crypto, ReaderFactory readerFactory,
WriterFactory writerFactory, DuplexPlugin plugin,
PseudoRandom random, ConnectionCallback callback,
AtomicBoolean connected, AtomicBoolean succeeded) {
super(crypto, readerFactory, writerFactory, plugin, random, callback,
connected, succeeded);
}
@Override
public void run() {
// Try an incoming connection first, then an outgoing connection
long halfTime = System.currentTimeMillis() + INVITATION_TIMEOUT / 2;
DuplexTransportConnection conn = acceptIncomingConnection();
if(conn == null) conn = makeOutgoingConnection(halfTime);
if(conn == null) {
waitForHalfTime(halfTime);
conn = makeOutgoingConnection();
}
if(conn == null) return;
if(LOG.isLoggable(INFO)) LOG.info(pluginName + " connected");
// FIXME: Carry out the real invitation protocol
// Carry out the key agreement protocol
InputStream in;
OutputStream out;
Reader r;
Writer w;
byte[] secret;
try {
in = conn.getInputStream();
OutputStream out = conn.getOutputStream();
byte[] hash = new byte[HASH_LENGTH];
int offset = 0;
while(offset < hash.length) {
int read = in.read(hash, offset, hash.length - offset);
if(read == -1) break;
offset += read;
}
if(offset < HASH_LENGTH) throw new EOFException();
if(LOG.isLoggable(INFO)) LOG.info(pluginName + " received hash");
// Don't proceed with more than one connection
if(connected.getAndSet(true)) {
if(LOG.isLoggable(INFO))
LOG.info(pluginName + " redundant");
tryToClose(conn, false);
return;
}
out.write(hash);
out.flush();
if(LOG.isLoggable(INFO)) LOG.info(pluginName + " sent hash");
succeeded.set(true);
callback.connectionEstablished(123456, 123456,
new ConfirmationSender(out));
out = conn.getOutputStream();
r = readerFactory.createReader(in);
w = writerFactory.createWriter(out);
// Alice goes first
byte[] hash = receivePublicKeyHash(r);
sendPublicKeyHash(w);
byte[] key = receivePublicKey(r);
sendPublicKey(w);
secret = deriveSharedSecret(hash, key, false);
} catch(IOException e) {
if(LOG.isLoggable(WARNING)) LOG.warning(e.toString());
tryToClose(conn, true);
return;
} catch(GeneralSecurityException e) {
if(LOG.isLoggable(WARNING)) LOG.warning(e.toString());
tryToClose(conn, true);
return;
}
// The key agreement succeeded
if(LOG.isLoggable(INFO)) LOG.info(pluginName + " succeeded");
succeeded.set(true);
// Derive the confirmation codes
int[] codes = crypto.deriveConfirmationCodes(secret);
callback.connectionEstablished(codes[1], codes[0],
new ConfirmationSender(w));
// Check whether the remote peer's confirmation codes matched
try {
if(in.read() == 1) callback.codesMatch();
if(r.readBoolean()) callback.codesMatch();
else callback.codesDoNotMatch();
} catch(IOException e) {
if(LOG.isLoggable(WARNING)) LOG.warning(e.toString());
@@ -87,35 +88,4 @@ class BobConnector extends Thread {
callback.codesDoNotMatch();
}
}
private DuplexTransportConnection acceptIncomingConnection() {
if(LOG.isLoggable(INFO))
LOG.info(pluginName + " accepting incoming connection");
return plugin.acceptInvitation(random, INVITATION_TIMEOUT / 2);
}
private DuplexTransportConnection makeOutgoingConnection(long halfTime) {
long now = System.currentTimeMillis();
if(now < halfTime) {
if(LOG.isLoggable(INFO))
LOG.info(pluginName + " sleeping until half-time");
try {
Thread.sleep(halfTime - now);
} catch(InterruptedException e) {
if(LOG.isLoggable(INFO)) LOG.info("Interrupted while sleeping");
return null;
}
}
if(LOG.isLoggable(INFO))
LOG.info(pluginName + " making outgoing connection");
return plugin.sendInvitation(random, INVITATION_TIMEOUT / 2);
}
private void tryToClose(DuplexTransportConnection conn, boolean exception) {
try {
conn.dispose(exception, true);
} catch(IOException e) {
if(LOG.isLoggable(WARNING)) LOG.warning(e.toString());
}
}
}

View File

@@ -1,38 +0,0 @@
package net.sf.briar.invitation;
import static java.util.logging.Level.WARNING;
import java.io.IOException;
import java.io.OutputStream;
import java.util.logging.Logger;
import net.sf.briar.api.invitation.ConfirmationCallback;
class ConfirmationSender implements ConfirmationCallback {
private static final Logger LOG =
Logger.getLogger(ConfirmationSender.class.getName());
private final OutputStream out;
ConfirmationSender(OutputStream out) {
this.out = out;
}
public void codesMatch() {
write(1);
}
public void codesDoNotMatch() {
write(0);
}
private void write(int b) {
try {
out.write(b);
out.flush();
} catch(IOException e) {
if(LOG.isLoggable(WARNING)) LOG.warning(e.toString());
}
}
}

View File

@@ -0,0 +1,169 @@
package net.sf.briar.invitation;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
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_PUBLIC_KEY_LENGTH;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Logger;
import net.sf.briar.api.FormatException;
import net.sf.briar.api.crypto.CryptoComponent;
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.api.invitation.ConfirmationCallback;
import net.sf.briar.api.invitation.ConnectionCallback;
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;
abstract class Connector extends Thread {
private static final Logger LOG =
Logger.getLogger(Connector.class.getName());
protected final CryptoComponent crypto;
protected final ReaderFactory readerFactory;
protected final WriterFactory writerFactory;
protected final DuplexPlugin plugin;
protected final PseudoRandom random;
protected final ConnectionCallback callback;
protected final AtomicBoolean connected, succeeded;
protected final String pluginName;
private final KeyPair keyPair;
private final KeyParser keyParser;
private final MessageDigest messageDigest;
Connector(CryptoComponent crypto, ReaderFactory readerFactory,
WriterFactory writerFactory, DuplexPlugin plugin,
PseudoRandom random, ConnectionCallback callback,
AtomicBoolean connected, AtomicBoolean succeeded) {
this.crypto = crypto;
this.readerFactory = readerFactory;
this.writerFactory = writerFactory;
this.plugin = plugin;
this.random = random;
this.callback = callback;
this.connected = connected;
this.succeeded = succeeded;
pluginName = plugin.getClass().getName();
keyPair = crypto.generateAgreementKeyPair();
keyParser = crypto.getAgreementKeyParser();
messageDigest = crypto.getMessageDigest();
}
protected DuplexTransportConnection acceptIncomingConnection() {
if(LOG.isLoggable(INFO))
LOG.info(pluginName + " accepting incoming connection");
return plugin.acceptInvitation(random, INVITATION_TIMEOUT / 2);
}
protected DuplexTransportConnection makeOutgoingConnection() {
if(LOG.isLoggable(INFO))
LOG.info(pluginName + " making outgoing connection");
return plugin.sendInvitation(random, INVITATION_TIMEOUT / 2);
}
protected void waitForHalfTime(long halfTime) {
long now = System.currentTimeMillis();
if(now < halfTime) {
if(LOG.isLoggable(INFO))
LOG.info(pluginName + " sleeping until half-time");
try {
Thread.sleep(halfTime - now);
} catch(InterruptedException e) {
if(LOG.isLoggable(INFO)) LOG.info("Interrupted while sleeping");
Thread.currentThread().interrupt();
return;
}
}
}
protected void tryToClose(DuplexTransportConnection conn,
boolean exception) {
try {
conn.dispose(exception, true);
} catch(IOException e) {
if(LOG.isLoggable(WARNING)) LOG.warning(e.toString());
}
}
protected void sendPublicKeyHash(Writer w) throws IOException {
w.writeBytes(messageDigest.digest(keyPair.getPublic().getEncoded()));
w.flush();
if(LOG.isLoggable(INFO)) LOG.info(pluginName + " sent hash");
}
protected byte[] receivePublicKeyHash(Reader r) throws IOException {
byte[] b = r.readBytes(HASH_LENGTH);
if(b.length != HASH_LENGTH) throw new FormatException();
if(LOG.isLoggable(INFO)) LOG.info(pluginName + " received hash");
return b;
}
protected void sendPublicKey(Writer w) throws IOException {
w.writeBytes(keyPair.getPublic().getEncoded());
w.flush();
if(LOG.isLoggable(INFO)) LOG.info(pluginName + " sent key");
}
protected byte[] receivePublicKey(Reader r) throws IOException {
byte[] b = r.readBytes(MAX_PUBLIC_KEY_LENGTH);
try {
keyParser.parsePublicKey(b);
} catch(GeneralSecurityException e) {
throw new FormatException();
}
if(LOG.isLoggable(INFO)) LOG.info(pluginName + " received hash");
return b;
}
protected byte[] deriveSharedSecret(byte[] hash, byte[] key, boolean alice)
throws GeneralSecurityException {
// Check that the hash matches the key
if(!Arrays.equals(hash, messageDigest.digest(key))) {
if(LOG.isLoggable(INFO))
LOG.info(pluginName + " hash does not match key");
throw new GeneralSecurityException();
}
// Derive the shared secret
return crypto.deriveInitialSecret(key, keyPair, alice);
}
protected static class ConfirmationSender implements ConfirmationCallback {
private final Writer writer;
protected ConfirmationSender(Writer writer) {
this.writer = writer;
}
public void codesMatch() {
write(true);
}
public void codesDoNotMatch() {
write(false);
}
private void write(boolean match) {
try {
writer.writeBoolean(match);
writer.flush();
} catch(IOException e) {
if(LOG.isLoggable(WARNING)) LOG.warning(e.toString());
}
}
}
}

View File

@@ -10,17 +10,24 @@ import net.sf.briar.api.invitation.ConnectionCallback;
import net.sf.briar.api.invitation.InvitationManager;
import net.sf.briar.api.plugins.PluginManager;
import net.sf.briar.api.plugins.duplex.DuplexPlugin;
import net.sf.briar.api.serial.ReaderFactory;
import net.sf.briar.api.serial.WriterFactory;
import com.google.inject.Inject;
class InvitationManagerImpl implements InvitationManager {
private final CryptoComponent crypto;
private final ReaderFactory readerFactory;
private final WriterFactory writerFactory;
private final PluginManager pluginManager;
@Inject
InvitationManagerImpl(CryptoComponent crypto, PluginManager pluginManager) {
InvitationManagerImpl(CryptoComponent crypto, ReaderFactory readerFactory,
WriterFactory writerFactory, PluginManager pluginManager) {
this.crypto = crypto;
this.readerFactory = readerFactory;
this.writerFactory = writerFactory;
this.pluginManager = pluginManager;
}
@@ -41,7 +48,8 @@ class InvitationManagerImpl implements InvitationManager {
Collection<Thread> workers = new ArrayList<Thread>();
for(DuplexPlugin p : plugins) {
PseudoRandom r = crypto.getPseudoRandom(localCode, remoteCode);
Thread worker = new AliceConnector(p, r, c, connected, succeeded);
Thread worker = new AliceConnector(crypto, readerFactory,
writerFactory, p, r, c, connected, succeeded);
workers.add(worker);
worker.start();
}
@@ -55,7 +63,8 @@ class InvitationManagerImpl implements InvitationManager {
Collection<Thread> workers = new ArrayList<Thread>();
for(DuplexPlugin p : plugins) {
PseudoRandom r = crypto.getPseudoRandom(remoteCode, localCode);
Thread worker = new BobConnector(p, r, c, connected, succeeded);
Thread worker = new BobConnector(crypto, readerFactory,
writerFactory, p, r, c, connected, succeeded);
workers.add(worker);
worker.start();
}

View File

@@ -22,6 +22,14 @@ class WriterImpl implements Writer {
this.out = out;
}
public void flush() throws IOException {
out.flush();
}
public void close() throws IOException {
out.close();
}
public void addConsumer(Consumer c) {
consumers.add(c);
}