mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-16 12:49:55 +01:00
Merged some redundant code.
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package net.sf.briar.api.crypto;
|
package net.sf.briar.api.crypto;
|
||||||
|
|
||||||
import java.security.KeyPair;
|
import java.security.KeyPair;
|
||||||
|
import java.security.PrivateKey;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.security.Signature;
|
import java.security.Signature;
|
||||||
|
|
||||||
@@ -15,8 +16,8 @@ public interface CryptoComponent {
|
|||||||
|
|
||||||
ErasableKey deriveMacKey(byte[] secret, boolean initiator);
|
ErasableKey deriveMacKey(byte[] secret, boolean initiator);
|
||||||
|
|
||||||
byte[][] deriveInitialSecrets(byte[] theirPublicKey, KeyPair ourKeyPair,
|
byte[][] deriveInitialSecrets(byte[] ourPublicKey, byte[] theirPublicKey,
|
||||||
int invitationCode, boolean initiator);
|
PrivateKey ourPrivateKey, int invitationCode, boolean initiator);
|
||||||
|
|
||||||
int deriveConfirmationCode(byte[] secret, boolean initiator);
|
int deriveConfirmationCode(byte[] secret, boolean initiator);
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import static net.sf.briar.api.plugins.InvitationConstants.CODE_BITS;
|
|||||||
import java.security.GeneralSecurityException;
|
import java.security.GeneralSecurityException;
|
||||||
import java.security.KeyPair;
|
import java.security.KeyPair;
|
||||||
import java.security.KeyPairGenerator;
|
import java.security.KeyPairGenerator;
|
||||||
|
import java.security.PrivateKey;
|
||||||
import java.security.PublicKey;
|
import java.security.PublicKey;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.security.Security;
|
import java.security.Security;
|
||||||
@@ -124,16 +125,16 @@ class CryptoComponentImpl implements CryptoComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[][] deriveInitialSecrets(byte[] theirPublicKey,
|
public byte[][] deriveInitialSecrets(byte[] ourPublicKey,
|
||||||
KeyPair ourKeyPair, int invitationCode, boolean initiator) {
|
byte[] theirPublicKey, PrivateKey ourPrivateKey, int invitationCode,
|
||||||
|
boolean initiator) {
|
||||||
try {
|
try {
|
||||||
PublicKey theirPublic = keyParser.parsePublicKey(theirPublicKey);
|
PublicKey theirPublic = keyParser.parsePublicKey(theirPublicKey);
|
||||||
MessageDigest messageDigest = getMessageDigest();
|
MessageDigest messageDigest = getMessageDigest();
|
||||||
byte[] ourPublicKey = ourKeyPair.getPublic().getEncoded();
|
|
||||||
byte[] ourHash = messageDigest.digest(ourPublicKey);
|
byte[] ourHash = messageDigest.digest(ourPublicKey);
|
||||||
byte[] theirHash = messageDigest.digest(theirPublicKey);
|
byte[] theirHash = messageDigest.digest(theirPublicKey);
|
||||||
// The initiator and responder info for the KDF are the hashes of
|
// The initiator and responder info for the concatenation KDF are
|
||||||
// the corresponding public keys
|
// the hashes of the corresponding public keys
|
||||||
byte[] initiatorInfo, responderInfo;
|
byte[] initiatorInfo, responderInfo;
|
||||||
if(initiator) {
|
if(initiator) {
|
||||||
initiatorInfo = ourHash;
|
initiatorInfo = ourHash;
|
||||||
@@ -142,20 +143,23 @@ class CryptoComponentImpl implements CryptoComponent {
|
|||||||
initiatorInfo = theirHash;
|
initiatorInfo = theirHash;
|
||||||
responderInfo = ourHash;
|
responderInfo = ourHash;
|
||||||
}
|
}
|
||||||
// The public info for the KDF is the invitation code as a uint32
|
// The public info for the concatenation KDF is the invitation code
|
||||||
|
// as a uint32
|
||||||
byte[] publicInfo = new byte[4];
|
byte[] publicInfo = new byte[4];
|
||||||
ByteUtils.writeUint32(invitationCode, publicInfo, 0);
|
ByteUtils.writeUint32(invitationCode, publicInfo, 0);
|
||||||
// The raw secret comes from the key agreement algorithm
|
// The raw secret comes from the key agreement algorithm
|
||||||
KeyAgreement keyAgreement = KeyAgreement.getInstance(
|
KeyAgreement keyAgreement = KeyAgreement.getInstance(
|
||||||
KEY_AGREEMENT_ALGO, PROVIDER);
|
KEY_AGREEMENT_ALGO, PROVIDER);
|
||||||
keyAgreement.init(ourKeyPair.getPrivate());
|
keyAgreement.init(ourPrivateKey);
|
||||||
keyAgreement.doPhase(theirPublic, true);
|
keyAgreement.doPhase(theirPublic, true);
|
||||||
byte[] rawSecret = keyAgreement.generateSecret();
|
byte[] rawSecret = keyAgreement.generateSecret();
|
||||||
// Derive the cooked secret from the raw secret
|
// Derive the cooked secret from the raw secret using the
|
||||||
|
// concatenation KDF
|
||||||
byte[] cookedSecret = concatenationKdf(rawSecret, FIRST,
|
byte[] cookedSecret = concatenationKdf(rawSecret, FIRST,
|
||||||
initiatorInfo, responderInfo, publicInfo);
|
initiatorInfo, responderInfo, publicInfo);
|
||||||
ByteUtils.erase(rawSecret);
|
ByteUtils.erase(rawSecret);
|
||||||
// Derive the incoming and outgoing secrets from the cooked secret
|
// Derive the incoming and outgoing secrets from the cooked secret
|
||||||
|
// using the CTR mode KDF
|
||||||
byte[][] secrets = new byte[2][];
|
byte[][] secrets = new byte[2][];
|
||||||
secrets[0] = counterModeKdf(cookedSecret, FIRST, INITIATOR);
|
secrets[0] = counterModeKdf(cookedSecret, FIRST, INITIATOR);
|
||||||
secrets[1] = counterModeKdf(cookedSecret, FIRST, RESPONDER);
|
secrets[1] = counterModeKdf(cookedSecret, FIRST, RESPONDER);
|
||||||
@@ -166,7 +170,7 @@ class CryptoComponentImpl implements CryptoComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Key derivation function based on a hash function - see NIST SP 800-65A,
|
// Key derivation function based on a hash function - see NIST SP 800-56A,
|
||||||
// section 5.8
|
// section 5.8
|
||||||
private byte[] concatenationKdf(byte[] rawSecret, byte[] label,
|
private byte[] concatenationKdf(byte[] rawSecret, byte[] label,
|
||||||
byte[] initiatorInfo, byte[] responderInfo, byte[] publicInfo) {
|
byte[] initiatorInfo, byte[] responderInfo, byte[] publicInfo) {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import net.sf.briar.api.crypto.PseudoRandom;
|
|||||||
import net.sf.briar.api.db.DatabaseComponent;
|
import net.sf.briar.api.db.DatabaseComponent;
|
||||||
import net.sf.briar.api.db.DbException;
|
import net.sf.briar.api.db.DbException;
|
||||||
import net.sf.briar.api.plugins.IncomingInvitationCallback;
|
import net.sf.briar.api.plugins.IncomingInvitationCallback;
|
||||||
|
import net.sf.briar.api.plugins.InvitationCallback;
|
||||||
import net.sf.briar.api.plugins.InvitationStarter;
|
import net.sf.briar.api.plugins.InvitationStarter;
|
||||||
import net.sf.briar.api.plugins.OutgoingInvitationCallback;
|
import net.sf.briar.api.plugins.OutgoingInvitationCallback;
|
||||||
import net.sf.briar.api.plugins.PluginExecutor;
|
import net.sf.briar.api.plugins.PluginExecutor;
|
||||||
@@ -31,7 +32,6 @@ import net.sf.briar.api.serial.Writer;
|
|||||||
import net.sf.briar.api.serial.WriterFactory;
|
import net.sf.briar.api.serial.WriterFactory;
|
||||||
import net.sf.briar.util.ByteUtils;
|
import net.sf.briar.util.ByteUtils;
|
||||||
|
|
||||||
// FIXME: Refactor this class to remove duplicated code
|
|
||||||
class InvitationStarterImpl implements InvitationStarter {
|
class InvitationStarterImpl implements InvitationStarter {
|
||||||
|
|
||||||
private static final String TIMED_OUT = "INVITATION_TIMED_OUT";
|
private static final String TIMED_OUT = "INVITATION_TIMED_OUT";
|
||||||
@@ -57,122 +57,80 @@ class InvitationStarterImpl implements InvitationStarter {
|
|||||||
this.writerFactory = writerFactory;
|
this.writerFactory = writerFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void startIncomingInvitation(final DuplexPlugin plugin,
|
public void startIncomingInvitation(DuplexPlugin plugin,
|
||||||
final IncomingInvitationCallback callback) {
|
IncomingInvitationCallback callback) {
|
||||||
pluginExecutor.execute(new Runnable() {
|
pluginExecutor.execute(new IncomingInvitationWorker(plugin, callback));
|
||||||
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,
|
public void startOutgoingInvitation(DuplexPlugin plugin,
|
||||||
final OutgoingInvitationCallback callback) {
|
OutgoingInvitationCallback callback) {
|
||||||
pluginExecutor.execute(new Runnable() {
|
pluginExecutor.execute(new OutgoingInvitationWorker(plugin, callback));
|
||||||
public void run() {
|
}
|
||||||
// Generate an invitation code and use it to seed the PRNG
|
|
||||||
int code = crypto.getSecureRandom().nextInt(MAX_CODE + 1);
|
private abstract class InvitationWorker implements Runnable {
|
||||||
PseudoRandom r = crypto.getPseudoRandom(code);
|
|
||||||
// Connect to the invitee
|
private final DuplexPlugin plugin;
|
||||||
DuplexTransportConnection conn = plugin.sendInvitation(r,
|
private final InvitationCallback callback;
|
||||||
INVITATION_TIMEOUT);
|
private final boolean initiator;
|
||||||
if(callback.isCancelled()) {
|
|
||||||
if(conn != null) conn.dispose(false, false);
|
protected InvitationWorker(DuplexPlugin plugin,
|
||||||
return;
|
InvitationCallback callback, boolean initiator) {
|
||||||
}
|
this.plugin = plugin;
|
||||||
if(conn == null) {
|
this.callback = callback;
|
||||||
callback.showFailure(TIMED_OUT);
|
this.initiator = initiator;
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
KeyPair ourKeyPair = crypto.generateKeyPair();
|
protected abstract int getInvitationCode();
|
||||||
MessageDigest messageDigest = crypto.getMessageDigest();
|
|
||||||
byte[] ourKey = ourKeyPair.getPublic().getEncoded();
|
public void run() {
|
||||||
byte[] ourHash = messageDigest.digest(ourKey);
|
long end = System.currentTimeMillis() + INVITATION_TIMEOUT;
|
||||||
byte[] theirKey, theirHash;
|
// Use the invitation code to seed the PRNG
|
||||||
try {
|
int code = getInvitationCode();
|
||||||
|
if(code == -1) return; // Cancelled
|
||||||
|
PseudoRandom r = crypto.getPseudoRandom(code);
|
||||||
|
long timeout = end - System.currentTimeMillis();
|
||||||
|
if(timeout <= 0) {
|
||||||
|
callback.showFailure(TIMED_OUT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Create a connection
|
||||||
|
DuplexTransportConnection conn;
|
||||||
|
if(initiator) conn = plugin.sendInvitation(r, timeout);
|
||||||
|
else conn = plugin.acceptInvitation(r, timeout);
|
||||||
|
if(callback.isCancelled()) {
|
||||||
|
if(conn != null) conn.dispose(false, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(conn == null) {
|
||||||
|
callback.showFailure(TIMED_OUT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Use an ephemeral key pair for key agreement
|
||||||
|
KeyPair ourKeyPair = crypto.generateKeyPair();
|
||||||
|
MessageDigest messageDigest = crypto.getMessageDigest();
|
||||||
|
byte[] ourKey = ourKeyPair.getPublic().getEncoded();
|
||||||
|
byte[] ourHash = messageDigest.digest(ourKey);
|
||||||
|
byte[] theirKey, theirHash;
|
||||||
|
try {
|
||||||
|
OutputStream out = conn.getOutputStream();
|
||||||
|
Writer writer = writerFactory.createWriter(out);
|
||||||
|
InputStream in = conn.getInputStream();
|
||||||
|
Reader reader = readerFactory.createReader(in);
|
||||||
|
if(initiator) {
|
||||||
|
// Send the public key hash
|
||||||
|
writer.writeBytes(ourHash);
|
||||||
|
out.flush();
|
||||||
|
// Receive the public key hash
|
||||||
|
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);
|
||||||
|
} else {
|
||||||
// Receive the public key hash
|
// Receive the public key hash
|
||||||
InputStream in = conn.getInputStream();
|
|
||||||
Reader reader = readerFactory.createReader(in);
|
|
||||||
theirHash = reader.readBytes(HASH_LENGTH);
|
theirHash = reader.readBytes(HASH_LENGTH);
|
||||||
// Send the public key hash
|
// Send the public key hash
|
||||||
OutputStream out = conn.getOutputStream();
|
|
||||||
Writer writer = writerFactory.createWriter(out);
|
|
||||||
writer.writeBytes(ourHash);
|
writer.writeBytes(ourHash);
|
||||||
out.flush();
|
out.flush();
|
||||||
// Receive the public key
|
// Receive the public key
|
||||||
@@ -180,46 +138,83 @@ class InvitationStarterImpl implements InvitationStarter {
|
|||||||
// Send the public key
|
// Send the public key
|
||||||
writer.writeBytes(ourKey);
|
writer.writeBytes(ourKey);
|
||||||
out.flush();
|
out.flush();
|
||||||
} catch(IOException e) {
|
|
||||||
conn.dispose(true, false);
|
|
||||||
callback.showFailure(IO_EXCEPTION);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
conn.dispose(false, false);
|
} catch(IOException e) {
|
||||||
if(callback.isCancelled()) return;
|
conn.dispose(true, false);
|
||||||
// Check that the received hash matches the received key
|
callback.showFailure(IO_EXCEPTION);
|
||||||
if(!Arrays.equals(theirHash, messageDigest.digest(theirKey))) {
|
return;
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
});
|
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(ourKey, theirKey,
|
||||||
|
ourKeyPair.getPrivate(), code, initiator);
|
||||||
|
if(secrets == null) {
|
||||||
|
callback.showFailure(INVALID_KEY);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int initCode = crypto.deriveConfirmationCode(secrets[0], true);
|
||||||
|
int respCode = crypto.deriveConfirmationCode(secrets[1], false);
|
||||||
|
int ourCode = initiator ? initCode : respCode;
|
||||||
|
int theirCode = initiator ? respCode : initCode;
|
||||||
|
// 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
|
||||||
|
byte[] inSecret = initiator ? secrets[1] : secrets[0];
|
||||||
|
byte[] outSecret = initiator ? secrets[0] : secrets[1];
|
||||||
|
try {
|
||||||
|
db.addContact(inSecret, outSecret);
|
||||||
|
} catch(DbException e) {
|
||||||
|
callback.showFailure(DB_EXCEPTION);
|
||||||
|
ByteUtils.erase(secrets[0]);
|
||||||
|
ByteUtils.erase(secrets[1]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callback.showSuccess();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class IncomingInvitationWorker extends InvitationWorker {
|
||||||
|
|
||||||
|
private final IncomingInvitationCallback callback;
|
||||||
|
|
||||||
|
IncomingInvitationWorker(DuplexPlugin plugin,
|
||||||
|
IncomingInvitationCallback callback) {
|
||||||
|
super(plugin, callback, false);
|
||||||
|
this.callback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int getInvitationCode() {
|
||||||
|
return callback.enterInvitationCode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class OutgoingInvitationWorker extends InvitationWorker {
|
||||||
|
|
||||||
|
private final OutgoingInvitationCallback callback;
|
||||||
|
|
||||||
|
OutgoingInvitationWorker(DuplexPlugin plugin,
|
||||||
|
OutgoingInvitationCallback callback) {
|
||||||
|
super(plugin, callback, true);
|
||||||
|
this.callback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int getInvitationCode() {
|
||||||
|
int code = crypto.getSecureRandom().nextInt(MAX_CODE + 1);
|
||||||
|
callback.showInvitationCode(code);
|
||||||
|
return code;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user