diff --git a/api/net/sf/briar/api/crypto/CryptoComponent.java b/api/net/sf/briar/api/crypto/CryptoComponent.java index 6801a40ed71e75d2a4c98bf856bd0f23305b58d2..70fe01c927ebd1ba3bd78d55b8d7df413706c8ef 100644 --- a/api/net/sf/briar/api/crypto/CryptoComponent.java +++ b/api/net/sf/briar/api/crypto/CryptoComponent.java @@ -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(); diff --git a/api/net/sf/briar/api/crypto/PseudoRandom.java b/api/net/sf/briar/api/crypto/PseudoRandom.java new file mode 100644 index 0000000000000000000000000000000000000000..6abca7c506ed2e559f843c7e87787f91ce076a68 --- /dev/null +++ b/api/net/sf/briar/api/crypto/PseudoRandom.java @@ -0,0 +1,6 @@ +package net.sf.briar.api.crypto; + +public interface PseudoRandom { + + byte[] nextBytes(int bytes); +} diff --git a/api/net/sf/briar/api/plugins/IncomingInvitationCallback.java b/api/net/sf/briar/api/plugins/IncomingInvitationCallback.java new file mode 100644 index 0000000000000000000000000000000000000000..64199a59c1e8e43357d0f7d7ff172898fec2e8a0 --- /dev/null +++ b/api/net/sf/briar/api/plugins/IncomingInvitationCallback.java @@ -0,0 +1,6 @@ +package net.sf.briar.api.plugins; + +public interface IncomingInvitationCallback extends InvitationCallback { + + int enterInvitationCode(); +} diff --git a/api/net/sf/briar/api/plugins/InvitationCallback.java b/api/net/sf/briar/api/plugins/InvitationCallback.java new file mode 100644 index 0000000000000000000000000000000000000000..fb37cf40dd6b840d805d0f4c750f53218d31611c --- /dev/null +++ b/api/net/sf/briar/api/plugins/InvitationCallback.java @@ -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(); +} diff --git a/api/net/sf/briar/api/plugins/InvitationConstants.java b/api/net/sf/briar/api/plugins/InvitationConstants.java new file mode 100644 index 0000000000000000000000000000000000000000..e1c113180173381f0e5c2842a4d63bdd3eb2b72d --- /dev/null +++ b/api/net/sf/briar/api/plugins/InvitationConstants.java @@ -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; +} diff --git a/api/net/sf/briar/api/plugins/InvitationStarter.java b/api/net/sf/briar/api/plugins/InvitationStarter.java new file mode 100644 index 0000000000000000000000000000000000000000..92c9acd65ade2b900bd8ee7808af5b4dc3d54e51 --- /dev/null +++ b/api/net/sf/briar/api/plugins/InvitationStarter.java @@ -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); +} diff --git a/api/net/sf/briar/api/plugins/OutgoingInvitationCallback.java b/api/net/sf/briar/api/plugins/OutgoingInvitationCallback.java new file mode 100644 index 0000000000000000000000000000000000000000..b80e82bc7c3beafd349d3e3deceb7962cebb0fb7 --- /dev/null +++ b/api/net/sf/briar/api/plugins/OutgoingInvitationCallback.java @@ -0,0 +1,6 @@ +package net.sf.briar.api.plugins; + +public interface OutgoingInvitationCallback extends InvitationCallback { + + void showInvitationCode(int code); +} diff --git a/api/net/sf/briar/api/plugins/PluginManager.java b/api/net/sf/briar/api/plugins/PluginManager.java index b2dbd44ae00ccffd22dee53f7f383354f63f0422..17887added8925eb1a738e5657fdc34075d9f9f8 100644 --- a/api/net/sf/briar/api/plugins/PluginManager.java +++ b/api/net/sf/briar/api/plugins/PluginManager.java @@ -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(); } diff --git a/api/net/sf/briar/api/plugins/duplex/DuplexPlugin.java b/api/net/sf/briar/api/plugins/duplex/DuplexPlugin.java index fed6e5fd63d822d8c3f2f5f7d61e304f55332124..2de756dfaeb870ed01443ed3fd012f7337efa329 100644 --- a/api/net/sf/briar/api/plugins/duplex/DuplexPlugin.java +++ b/api/net/sf/briar/api/plugins/duplex/DuplexPlugin.java @@ -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); } diff --git a/api/net/sf/briar/api/plugins/simplex/SimplexPlugin.java b/api/net/sf/briar/api/plugins/simplex/SimplexPlugin.java index 4b06f54f3b0fb9a033888450cfa3240ba2778d88..fd11d6646c3f6802b1d5c608de4577fdcf50b1cc 100644 --- a/api/net/sf/briar/api/plugins/simplex/SimplexPlugin.java +++ b/api/net/sf/briar/api/plugins/simplex/SimplexPlugin.java @@ -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); } diff --git a/components/net/sf/briar/crypto/CryptoComponentImpl.java b/components/net/sf/briar/crypto/CryptoComponentImpl.java index 11d525bb5041154ad32e13efc7137c8ead371978..7eadb47cb1d0ed49e1cbbc9c92b381f4fb7fd50d 100644 --- a/components/net/sf/briar/crypto/CryptoComponentImpl.java +++ b/components/net/sf/briar/crypto/CryptoComponentImpl.java @@ -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; } diff --git a/components/net/sf/briar/crypto/PseudoRandomImpl.java b/components/net/sf/briar/crypto/PseudoRandomImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..1f74586e4a829e9e58cc117ec3132c1b07dcd5fc --- /dev/null +++ b/components/net/sf/briar/crypto/PseudoRandomImpl.java @@ -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; + } +} diff --git a/components/net/sf/briar/plugins/InvitationStarterImpl.java b/components/net/sf/briar/plugins/InvitationStarterImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..b61f1e2f4eb48e1c3e61ef987770f9b44ff0a961 --- /dev/null +++ b/components/net/sf/briar/plugins/InvitationStarterImpl.java @@ -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(); + } + }); + } +} diff --git a/components/net/sf/briar/plugins/PluginManagerImpl.java b/components/net/sf/briar/plugins/PluginManagerImpl.java index 0b1b5532955eb9c5dfc0b4baf005337aab7fbe94..ab05b6de48f47c5fe0bec1a36ec412a67c3f0c4e 100644 --- a/components/net/sf/briar/plugins/PluginManagerImpl.java +++ b/components/net/sf/briar/plugins/PluginManagerImpl.java @@ -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; diff --git a/components/net/sf/briar/plugins/bluetooth/BluetoothPlugin.java b/components/net/sf/briar/plugins/bluetooth/BluetoothPlugin.java index ae4141b05b76fdd5c423375c4998818943908a25..f7a57a551efdd66837fa6907cc1632ad00e8d1cd 100644 --- a/components/net/sf/briar/plugins/bluetooth/BluetoothPlugin.java +++ b/components/net/sf/briar/plugins/bluetooth/BluetoothPlugin.java @@ -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) { diff --git a/components/net/sf/briar/plugins/file/FilePlugin.java b/components/net/sf/briar/plugins/file/FilePlugin.java index 5cd992a78af88b0838eacefcd263af88e9ac8b95..4a5987acf88ed05c1779fdb9e79a584324873de6 100644 --- a/components/net/sf/briar/plugins/file/FilePlugin.java +++ b/components/net/sf/briar/plugins/file/FilePlugin.java @@ -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) { + public SimplexTransportWriter sendInvitationResponse(PseudoRandom r, + long timeout) { if(!running) return null; - return createWriter(createInvitationFilename(code, true)); + return createWriter(createInvitationFilename(r, true)); } - public SimplexTransportReader acceptInvitationResponse(int code, + public SimplexTransportReader acceptInvitationResponse(PseudoRandom r, long timeout) { if(!running) return null; - String filename = createInvitationFilename(code, true); + 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 { diff --git a/components/net/sf/briar/plugins/socket/LanSocketPlugin.java b/components/net/sf/briar/plugins/socket/LanSocketPlugin.java index 53290881f4ac8af3067c10a12ea62af6e2133a46..2f4568775ba65770aaf7a4d5e1e783e5af8731fb 100644 --- a/components/net/sf/briar/plugins/socket/LanSocketPlugin.java +++ b/components/net/sf/briar/plugins/socket/LanSocketPlugin.java @@ -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 { diff --git a/components/net/sf/briar/plugins/socket/SimpleSocketPlugin.java b/components/net/sf/briar/plugins/socket/SimpleSocketPlugin.java index b77b5c3e8e4b9861ba7841b4c5525cd4e35819b9..4a828f491276d4ebcee6fc09bd365ff11f59d79d 100644 --- a/components/net/sf/briar/plugins/socket/SimpleSocketPlugin.java +++ b/components/net/sf/briar/plugins/socket/SimpleSocketPlugin.java @@ -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(); } } diff --git a/components/net/sf/briar/plugins/tor/TorPlugin.java b/components/net/sf/briar/plugins/tor/TorPlugin.java index e7eaaf51374d313fc25c594dc9927b2369084a9f..e1b61f38585348b78aeb3ede19875f461b577a32 100644 --- a/components/net/sf/briar/plugins/tor/TorPlugin.java +++ b/components/net/sf/briar/plugins/tor/TorPlugin.java @@ -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(); } } diff --git a/test/net/sf/briar/plugins/DuplexClientTest.java b/test/net/sf/briar/plugins/DuplexClientTest.java index dd051cf37b5fc4465fff1f54a1adb593ec5bc677..2e58e14ba81dd2fc25d59007c57e7d7e56d54929 100644 --- a/test/net/sf/briar/plugins/DuplexClientTest.java +++ b/test/net/sf/briar/plugins/DuplexClientTest.java @@ -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 { diff --git a/test/net/sf/briar/plugins/DuplexServerTest.java b/test/net/sf/briar/plugins/DuplexServerTest.java index 008ab8108fd465686515d4918c1453e68d508d9b..662f299b89a80e4822e77b39786c41a26ff6d5f1 100644 --- a/test/net/sf/briar/plugins/DuplexServerTest.java +++ b/test/net/sf/briar/plugins/DuplexServerTest.java @@ -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 { diff --git a/test/net/sf/briar/plugins/DuplexTest.java b/test/net/sf/briar/plugins/DuplexTest.java index 1eeb292b840123889201c394d0337f19ccdfb5ac..c247415e46079e5c59f91d570d6d08f0c8a8f1cd 100644 --- a/test/net/sf/briar/plugins/DuplexTest.java +++ b/test/net/sf/briar/plugins/DuplexTest.java @@ -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; + } + } } diff --git a/test/net/sf/briar/util/ByteUtilsTest.java b/test/net/sf/briar/util/ByteUtilsTest.java index 0c0334697204395197cd6e021f95280ec417592f..c672bc0e2af7e72bbcce2171f27a72952cbfa775 100644 --- a/test/net/sf/briar/util/ByteUtilsTest.java +++ b/test/net/sf/briar/util/ByteUtilsTest.java @@ -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)); + } + } } diff --git a/util/net/sf/briar/util/ByteUtils.java b/util/net/sf/briar/util/ByteUtils.java index 4871865437874801104b57b2d43f641b6b7e75e5..1c3f9bdc13d1ebb419ad97531d876eb2c4017190 100644 --- a/util/net/sf/briar/util/ByteUtils.java +++ b/util/net/sf/briar/util/ByteUtils.java @@ -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; + } }