mirror of
https://github.com/haveno-dex/haveno.git
synced 2025-07-25 07:55:21 -04:00
move network code to module
This commit is contained in:
parent
105a63847a
commit
c6ece486ed
384 changed files with 11571 additions and 21763 deletions
135
network/src/main/java/io/bitsquare/crypto/EncryptionService.java
Normal file
135
network/src/main/java/io/bitsquare/crypto/EncryptionService.java
Normal file
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
* This file is part of Bitsquare.
|
||||
*
|
||||
* Bitsquare is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bitsquare is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bitsquare. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package io.bitsquare.crypto;
|
||||
|
||||
import io.bitsquare.common.crypto.CryptoException;
|
||||
import io.bitsquare.common.crypto.CryptoUtil;
|
||||
import io.bitsquare.common.crypto.KeyRing;
|
||||
import io.bitsquare.common.crypto.PubKeyRing;
|
||||
import io.bitsquare.common.util.Utilities;
|
||||
import io.bitsquare.p2p.Message;
|
||||
import io.bitsquare.p2p.messaging.DecryptedMessageWithPubKey;
|
||||
import io.bitsquare.p2p.messaging.SealedAndSignedMessage;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.crypto.*;
|
||||
import javax.inject.Inject;
|
||||
import java.io.IOException;
|
||||
import java.security.*;
|
||||
|
||||
public class EncryptionService {
|
||||
private static final Logger log = LoggerFactory.getLogger(EncryptionService.class);
|
||||
|
||||
@Nullable
|
||||
private final KeyRing keyRing;
|
||||
|
||||
@Inject
|
||||
public EncryptionService(KeyRing keyRing) {
|
||||
this.keyRing = keyRing;
|
||||
}
|
||||
|
||||
|
||||
public SealedAndSignedMessage encryptAndSignMessage(PubKeyRing pubKeyRing, Message message) throws CryptoException {
|
||||
log.trace("encryptAndSignMessage message = " + message);
|
||||
//long ts = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
// Create symmetric key
|
||||
KeyGenerator keyGenerator = KeyGenerator.getInstance(CryptoUtil.SYM_ENCR_KEY_ALGO);
|
||||
// TODO consider 256 bit as key length
|
||||
keyGenerator.init(128);
|
||||
SecretKey secretKey = keyGenerator.generateKey();
|
||||
|
||||
// Encrypt secretKey with peers pubKey using SealedObject
|
||||
Cipher cipherAsym = Cipher.getInstance(CryptoUtil.ASYM_CIPHER);
|
||||
cipherAsym.init(Cipher.ENCRYPT_MODE, pubKeyRing.getMsgEncryptionPubKey());
|
||||
SealedObject sealedSecretKey = new SealedObject(secretKey, cipherAsym);
|
||||
|
||||
// Sign (hash of) message and pack it into SignedObject
|
||||
SignedObject signedMessage = new SignedObject(message, keyRing.getMsgSignatureKeyPair().getPrivate(), Signature.getInstance(CryptoUtil.MSG_SIGN_ALGO));
|
||||
|
||||
// // Encrypt signedMessage with secretKey using SealedObject
|
||||
Cipher cipherSym = Cipher.getInstance(CryptoUtil.SYM_CIPHER);
|
||||
cipherSym.init(Cipher.ENCRYPT_MODE, secretKey);
|
||||
SealedObject sealedMessage = new SealedObject(signedMessage, cipherSym);
|
||||
|
||||
SealedAndSignedMessage sealedAndSignedMessage = new SealedAndSignedMessage(sealedSecretKey,
|
||||
sealedMessage,
|
||||
keyRing.getMsgSignatureKeyPair().getPublic()
|
||||
);
|
||||
//log.trace("Encryption needed {} ms", System.currentTimeMillis() - ts);
|
||||
log.trace("sealedAndSignedMessage size " + Utilities.objectToByteArray(sealedAndSignedMessage).length);
|
||||
return sealedAndSignedMessage;
|
||||
} catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException
|
||||
| IllegalBlockSizeException | IOException | SignatureException e) {
|
||||
throw new CryptoException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public DecryptedMessageWithPubKey decryptAndVerifyMessage(SealedAndSignedMessage sealedAndSignedMessage) throws CryptoException {
|
||||
// long ts = System.currentTimeMillis();
|
||||
try {
|
||||
if (keyRing == null)
|
||||
throw new CryptoException("keyRing is null");
|
||||
|
||||
SealedObject sealedSecretKey = sealedAndSignedMessage.sealedSecretKey;
|
||||
SealedObject sealedMessage = sealedAndSignedMessage.sealedMessage;
|
||||
PublicKey signaturePubKey = sealedAndSignedMessage.signaturePubKey;
|
||||
|
||||
// Decrypt secretKey with my privKey
|
||||
Cipher cipherAsym = Cipher.getInstance(CryptoUtil.ASYM_CIPHER);
|
||||
cipherAsym.init(Cipher.DECRYPT_MODE, keyRing.getMsgEncryptionKeyPair().getPrivate());
|
||||
Object secretKeyObject = sealedSecretKey.getObject(cipherAsym);
|
||||
if (secretKeyObject instanceof SecretKey) {
|
||||
SecretKey secretKey = (SecretKey) secretKeyObject;
|
||||
|
||||
// Decrypt signedMessage with secretKey
|
||||
Cipher cipherSym = Cipher.getInstance(CryptoUtil.SYM_CIPHER);
|
||||
cipherSym.init(Cipher.DECRYPT_MODE, secretKey);
|
||||
Object signedMessageObject = sealedMessage.getObject(cipherSym);
|
||||
if (signedMessageObject instanceof SignedObject) {
|
||||
SignedObject signedMessage = (SignedObject) signedMessageObject;
|
||||
|
||||
// Verify message with peers pubKey
|
||||
if (signedMessage.verify(signaturePubKey, Signature.getInstance(CryptoUtil.MSG_SIGN_ALGO))) {
|
||||
// Get message
|
||||
Object messageObject = signedMessage.getObject();
|
||||
if (messageObject instanceof Message) {
|
||||
//log.trace("Decryption needed {} ms", System.currentTimeMillis() - ts);
|
||||
return new DecryptedMessageWithPubKey((Message) messageObject, signaturePubKey);
|
||||
} else {
|
||||
throw new CryptoException("messageObject is not instance of Message");
|
||||
}
|
||||
} else {
|
||||
throw new CryptoException("Signature is not valid");
|
||||
}
|
||||
} else {
|
||||
throw new CryptoException("signedMessageObject is not instance of SignedObject");
|
||||
}
|
||||
} else {
|
||||
throw new CryptoException("secretKeyObject is not instance of SecretKey");
|
||||
}
|
||||
} catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException | BadPaddingException |
|
||||
ClassNotFoundException | IllegalBlockSizeException | IOException | SignatureException e) {
|
||||
throw new CryptoException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* This file is part of Bitsquare.
|
||||
*
|
||||
* Bitsquare is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bitsquare is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bitsquare. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package io.bitsquare.crypto;
|
||||
|
||||
import com.google.inject.Singleton;
|
||||
import io.bitsquare.app.AppModule;
|
||||
import org.springframework.core.env.Environment;
|
||||
|
||||
public class EncryptionServiceModule extends AppModule {
|
||||
|
||||
public EncryptionServiceModule(Environment env) {
|
||||
super(env);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configure() {
|
||||
bind(EncryptionService.class).in(Singleton.class);
|
||||
}
|
||||
}
|
7
network/src/main/java/io/bitsquare/p2p/Message.java
Normal file
7
network/src/main/java/io/bitsquare/p2p/Message.java
Normal file
|
@ -0,0 +1,7 @@
|
|||
package io.bitsquare.p2p;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public interface Message extends Serializable {
|
||||
|
||||
}
|
55
network/src/main/java/io/bitsquare/p2p/P2PModule.java
Normal file
55
network/src/main/java/io/bitsquare/p2p/P2PModule.java
Normal file
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* This file is part of Bitsquare.
|
||||
*
|
||||
* Bitsquare is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bitsquare is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bitsquare. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package io.bitsquare.p2p;
|
||||
|
||||
import com.google.inject.Singleton;
|
||||
import com.google.inject.name.Names;
|
||||
import io.bitsquare.app.AppModule;
|
||||
import io.bitsquare.app.ProgramArguments;
|
||||
import io.bitsquare.p2p.seed.SeedNodesRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.core.env.Environment;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import static com.google.inject.name.Names.named;
|
||||
|
||||
|
||||
public class P2PModule extends AppModule {
|
||||
private static final Logger log = LoggerFactory.getLogger(P2PModule.class);
|
||||
|
||||
public P2PModule(Environment env) {
|
||||
super(env);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configure() {
|
||||
bind(SeedNodesRepository.class).in(Singleton.class);
|
||||
bind(P2PService.class).in(Singleton.class);
|
||||
|
||||
Boolean useLocalhost = env.getProperty(ProgramArguments.USE_LOCALHOST, boolean.class, false);
|
||||
bind(boolean.class).annotatedWith(Names.named(ProgramArguments.USE_LOCALHOST)).toInstance(useLocalhost);
|
||||
|
||||
File torDir = new File(env.getRequiredProperty(ProgramArguments.TOR_DIR));
|
||||
bind(File.class).annotatedWith(named(ProgramArguments.TOR_DIR)).toInstance(torDir);
|
||||
|
||||
Integer port = env.getProperty(ProgramArguments.PORT_KEY, int.class, Utils.findFreeSystemPort());
|
||||
bind(int.class).annotatedWith(Names.named(ProgramArguments.PORT_KEY)).toInstance(port);
|
||||
}
|
||||
}
|
628
network/src/main/java/io/bitsquare/p2p/P2PService.java
Normal file
628
network/src/main/java/io/bitsquare/p2p/P2PService.java
Normal file
|
@ -0,0 +1,628 @@
|
|||
package io.bitsquare.p2p;
|
||||
|
||||
import com.google.common.util.concurrent.FutureCallback;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.SettableFuture;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.name.Named;
|
||||
import io.bitsquare.app.ProgramArguments;
|
||||
import io.bitsquare.common.UserThread;
|
||||
import io.bitsquare.common.crypto.CryptoException;
|
||||
import io.bitsquare.common.crypto.KeyRing;
|
||||
import io.bitsquare.common.crypto.PubKeyRing;
|
||||
import io.bitsquare.crypto.EncryptionService;
|
||||
import io.bitsquare.p2p.messaging.*;
|
||||
import io.bitsquare.p2p.network.*;
|
||||
import io.bitsquare.p2p.routing.Neighbor;
|
||||
import io.bitsquare.p2p.routing.Routing;
|
||||
import io.bitsquare.p2p.routing.RoutingListener;
|
||||
import io.bitsquare.p2p.seed.SeedNodesRepository;
|
||||
import io.bitsquare.p2p.storage.HashSetChangedListener;
|
||||
import io.bitsquare.p2p.storage.ProtectedExpirableDataStorage;
|
||||
import io.bitsquare.p2p.storage.data.ExpirableMailboxPayload;
|
||||
import io.bitsquare.p2p.storage.data.ExpirablePayload;
|
||||
import io.bitsquare.p2p.storage.data.ProtectedData;
|
||||
import io.bitsquare.p2p.storage.data.ProtectedMailboxData;
|
||||
import io.bitsquare.p2p.storage.messages.DataSetMessage;
|
||||
import io.bitsquare.p2p.storage.messages.GetDataSetMessage;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.math.BigInteger;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PublicKey;
|
||||
import java.security.SignatureException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
/**
|
||||
* Represents our node in the P2P network
|
||||
*/
|
||||
public class P2PService {
|
||||
private static final Logger log = LoggerFactory.getLogger(P2PService.class);
|
||||
|
||||
private final EncryptionService encryptionService;
|
||||
private KeyRing keyRing;
|
||||
private final NetworkStatistics networkStatistics;
|
||||
|
||||
private final NetworkNode networkNode;
|
||||
private final Routing routing;
|
||||
private final ProtectedExpirableDataStorage dataStorage;
|
||||
private final List<DecryptedMailListener> decryptedMailListeners = new CopyOnWriteArrayList<>();
|
||||
private final List<DecryptedMailboxListener> decryptedMailboxListeners = new CopyOnWriteArrayList<>();
|
||||
private final List<P2PServiceListener> p2pServiceListeners = new CopyOnWriteArrayList<>();
|
||||
private final Map<DecryptedMessageWithPubKey, ProtectedMailboxData> mailboxMap = new ConcurrentHashMap<>();
|
||||
private volatile boolean shutDownInProgress;
|
||||
private List<Address> seedNodeAddresses;
|
||||
private List<Address> connectedSeedNodes = new CopyOnWriteArrayList<>();
|
||||
private Set<Address> authenticatedPeerAddresses = new HashSet<>();
|
||||
private boolean authenticatedToFirstPeer;
|
||||
private boolean allDataReceived;
|
||||
public boolean authenticated;
|
||||
private boolean shutDownComplete;
|
||||
private List<Runnable> shutDownResultHandlers = new CopyOnWriteArrayList<>();
|
||||
private final List<Long> getDataSetMessageNonceList = new ArrayList<>();
|
||||
private boolean allSeedNodesRequested;
|
||||
private Timer sendGetAllDataMessageTimer;
|
||||
private volatile boolean hiddenServiceReady;
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Constructor
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Inject
|
||||
public P2PService(SeedNodesRepository seedNodesRepository,
|
||||
@Named(ProgramArguments.PORT_KEY) int port,
|
||||
@Named(ProgramArguments.TOR_DIR) File torDir,
|
||||
@Named(ProgramArguments.USE_LOCALHOST) boolean useLocalhost,
|
||||
EncryptionService encryptionService,
|
||||
KeyRing keyRing) {
|
||||
this.encryptionService = encryptionService;
|
||||
this.keyRing = keyRing;
|
||||
|
||||
networkStatistics = new NetworkStatistics();
|
||||
|
||||
// network layer
|
||||
if (useLocalhost) {
|
||||
networkNode = new LocalhostNetworkNode(port);
|
||||
seedNodeAddresses = seedNodesRepository.getLocalhostSeedNodeAddresses();
|
||||
} else {
|
||||
networkNode = new TorNetworkNode(port, torDir);
|
||||
seedNodeAddresses = seedNodesRepository.getTorSeedNodeAddresses();
|
||||
}
|
||||
|
||||
// routing layer
|
||||
routing = new Routing(networkNode, seedNodeAddresses);
|
||||
|
||||
|
||||
// storage layer
|
||||
dataStorage = new ProtectedExpirableDataStorage(routing, encryptionService);
|
||||
|
||||
|
||||
// Listeners
|
||||
networkNode.addMessageListener((message, connection) -> {
|
||||
if (message instanceof GetDataSetMessage) {
|
||||
log.trace("Received GetAllDataMessage: " + message);
|
||||
|
||||
// we only reply if we did not get the message form ourselves (in case we are a seed node)
|
||||
if (!getDataSetMessageNonceList.contains(((GetDataSetMessage) message).nonce)) {
|
||||
networkNode.sendMessage(connection, new DataSetMessage(getHashSet()));
|
||||
} else {
|
||||
connection.shutDown(() -> {
|
||||
if (allSeedNodesRequested) dataReceived();
|
||||
});
|
||||
}
|
||||
} else if (message instanceof DataSetMessage) {
|
||||
log.trace("Received AllDataMessage: " + message);
|
||||
// we keep that connection open as the bootstrapping peer will use that for the authentication
|
||||
|
||||
// as we are not authenticated yet the data adding will not be broadcasted
|
||||
HashSet<ProtectedData> set = ((DataSetMessage) message).set;
|
||||
set.stream().forEach(e -> dataStorage.add(e, connection.getPeerAddress()));
|
||||
|
||||
set.stream().filter(e -> e instanceof ProtectedMailboxData).forEach(e -> tryDecryptMailboxData((ProtectedMailboxData) e));
|
||||
|
||||
dataReceived();
|
||||
} else if (message instanceof SealedAndSignedMessage) {
|
||||
try {
|
||||
DecryptedMessageWithPubKey decryptedMessageWithPubKey = encryptionService.decryptAndVerifyMessage((SealedAndSignedMessage) message);
|
||||
UserThread.execute(() -> decryptedMailListeners.stream().forEach(e -> e.onMailMessage(decryptedMessageWithPubKey, connection.getPeerAddress())));
|
||||
} catch (CryptoException e) {
|
||||
log.info("Decryption of SealedAndSignedMessage failed. That is expected if the message is not intended for us.");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
routing.addRoutingListener(new RoutingListener() {
|
||||
@Override
|
||||
public void onFirstNeighborAdded(Neighbor neighbor) {
|
||||
log.trace("onFirstNeighbor " + neighbor.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNeighborAdded(Neighbor neighbor) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNeighborRemoved(Address address) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectionAuthenticated(Connection connection) {
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
dataStorage.addHashSetChangedListener(new HashSetChangedListener() {
|
||||
@Override
|
||||
public void onAdded(ProtectedData entry) {
|
||||
if (entry instanceof ProtectedMailboxData)
|
||||
tryDecryptMailboxData((ProtectedMailboxData) entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRemoved(ProtectedData entry) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void protocol() {
|
||||
// networkNode.start
|
||||
// onTorNodeReady: sendGetAllDataMessage
|
||||
// onHiddenServiceReady: tryStartAuthentication
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// API
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// startup sequence
|
||||
// networkNode.start
|
||||
// SetupListener.onTorNodeReady: sendGetAllDataMessage
|
||||
// SetupListener.onHiddenServiceReady: tryStartAuthentication
|
||||
// if hiddenServiceReady && allDataReceived) routing.startAuthentication
|
||||
// ConnectionListener.onPeerAddressAuthenticated
|
||||
|
||||
public void start() {
|
||||
start(null);
|
||||
}
|
||||
|
||||
public void start(@Nullable P2PServiceListener listener) {
|
||||
if (listener != null)
|
||||
addP2PServiceListener(listener);
|
||||
|
||||
networkNode.start(new SetupListener() {
|
||||
@Override
|
||||
public void onTorNodeReady() {
|
||||
UserThread.execute(() -> p2pServiceListeners.stream().forEach(e -> e.onTorNodeReady()));
|
||||
|
||||
// we don't know yet our own address so we can not filter that from the
|
||||
// seedNodeAddresses in case we are a seed node
|
||||
sendGetAllDataMessage(seedNodeAddresses);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHiddenServiceReady() {
|
||||
hiddenServiceReady = true;
|
||||
tryStartAuthentication();
|
||||
|
||||
UserThread.execute(() -> p2pServiceListeners.stream().forEach(e -> e.onHiddenServiceReady()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetupFailed(Throwable throwable) {
|
||||
UserThread.execute(() -> p2pServiceListeners.stream().forEach(e -> e.onSetupFailed(throwable)));
|
||||
}
|
||||
});
|
||||
|
||||
networkNode.addConnectionListener(new ConnectionListener() {
|
||||
@Override
|
||||
public void onConnection(Connection connection) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPeerAddressAuthenticated(Address peerAddress, Connection connection) {
|
||||
authenticatedPeerAddresses.add(peerAddress);
|
||||
authenticatedToFirstPeer = true;
|
||||
|
||||
P2PService.this.authenticated = true;
|
||||
dataStorage.setAuthenticated(true);
|
||||
UserThread.execute(() -> p2pServiceListeners.stream().forEach(e -> e.onAuthenticated()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnect(Reason reason, Connection connection) {
|
||||
Address peerAddress = connection.getPeerAddress();
|
||||
if (peerAddress != null)
|
||||
authenticatedPeerAddresses.remove(peerAddress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable throwable) {
|
||||
log.error("onError self/ConnectionException " + networkNode.getAddress() + "/" + throwable);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void shutDown(Runnable shutDownCompleteHandler) {
|
||||
if (!shutDownInProgress) {
|
||||
shutDownInProgress = true;
|
||||
|
||||
shutDownResultHandlers.add(shutDownCompleteHandler);
|
||||
|
||||
if (sendGetAllDataMessageTimer != null)
|
||||
sendGetAllDataMessageTimer.cancel();
|
||||
|
||||
if (dataStorage != null)
|
||||
dataStorage.shutDown();
|
||||
|
||||
if (routing != null)
|
||||
routing.shutDown();
|
||||
|
||||
if (networkNode != null)
|
||||
networkNode.shutDown(() -> {
|
||||
UserThread.execute(() -> shutDownResultHandlers.stream().forEach(e -> new Thread(e).start()));
|
||||
shutDownComplete = true;
|
||||
});
|
||||
} else {
|
||||
if (shutDownComplete)
|
||||
new Thread(shutDownCompleteHandler).start();
|
||||
else
|
||||
shutDownResultHandlers.add(shutDownCompleteHandler);
|
||||
log.warn("shutDown already in progress");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isAuthenticated() {
|
||||
return authenticated;
|
||||
}
|
||||
|
||||
public void removeEntryFromMailbox(DecryptedMessageWithPubKey decryptedMessageWithPubKey) {
|
||||
log.trace("removeEntryFromMailbox");
|
||||
ProtectedMailboxData mailboxData = mailboxMap.get(decryptedMessageWithPubKey);
|
||||
if (mailboxData != null && mailboxData.expirablePayload instanceof ExpirableMailboxPayload) {
|
||||
checkArgument(mailboxData.receiversPubKey.equals(keyRing.getStorageSignatureKeyPair().getPublic()),
|
||||
"mailboxData.receiversPubKey is not matching with our key. That must not happen.");
|
||||
removeMailboxData((ExpirableMailboxPayload) mailboxData.expirablePayload, mailboxData.receiversPubKey);
|
||||
mailboxMap.remove(decryptedMessageWithPubKey);
|
||||
log.trace("Removed successfully protectedExpirableData.");
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Messaging
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public void sendEncryptedMailMessage(Address peerAddress, PubKeyRing pubKeyRing, MailMessage message, SendMailMessageListener sendMailMessageListener) {
|
||||
checkNotNull(peerAddress, "PeerAddress must not be null (sendEncryptedMailMessage)");
|
||||
|
||||
if (!authenticatedToFirstPeer)
|
||||
throw new AuthenticationException("You must be authenticated before sending direct messages.");
|
||||
|
||||
if (!authenticatedPeerAddresses.contains(peerAddress))
|
||||
routing.authenticateToPeer(peerAddress,
|
||||
() -> doSendEncryptedMailMessage(peerAddress, pubKeyRing, message, sendMailMessageListener),
|
||||
() -> UserThread.execute(() -> sendMailMessageListener.onFault()));
|
||||
else
|
||||
doSendEncryptedMailMessage(peerAddress, pubKeyRing, message, sendMailMessageListener);
|
||||
}
|
||||
|
||||
private void doSendEncryptedMailMessage(Address peerAddress, PubKeyRing pubKeyRing, MailMessage message, SendMailMessageListener sendMailMessageListener) {
|
||||
try {
|
||||
SealedAndSignedMessage sealedAndSignedMessage = encryptionService.encryptAndSignMessage(pubKeyRing, message);
|
||||
SettableFuture<Connection> future = sendMessage(peerAddress, sealedAndSignedMessage);
|
||||
Futures.addCallback(future, new FutureCallback<Connection>() {
|
||||
@Override
|
||||
public void onSuccess(@Nullable Connection connection) {
|
||||
UserThread.execute(() -> sendMailMessageListener.onArrived());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable throwable) {
|
||||
throwable.printStackTrace();
|
||||
UserThread.execute(() -> sendMailMessageListener.onFault());
|
||||
}
|
||||
});
|
||||
} catch (CryptoException e) {
|
||||
e.printStackTrace();
|
||||
UserThread.execute(() -> sendMailMessageListener.onFault());
|
||||
}
|
||||
}
|
||||
|
||||
public void sendEncryptedMailboxMessage(Address peerAddress, PubKeyRing peersPubKeyRing, MailboxMessage message, SendMailboxMessageListener sendMailboxMessageListener) {
|
||||
checkNotNull(peerAddress, "PeerAddress must not be null (sendEncryptedMailboxMessage)");
|
||||
checkArgument(!keyRing.getPubKeyRing().equals(peersPubKeyRing), "We got own keyring instead of that from peer");
|
||||
|
||||
if (!authenticatedToFirstPeer)
|
||||
throw new AuthenticationException("You must be authenticated before sending direct messages.");
|
||||
|
||||
if (authenticatedPeerAddresses.contains(peerAddress)) {
|
||||
trySendEncryptedMailboxMessage(peerAddress, peersPubKeyRing, message, sendMailboxMessageListener);
|
||||
} else {
|
||||
routing.authenticateToPeer(peerAddress,
|
||||
() -> trySendEncryptedMailboxMessage(peerAddress, peersPubKeyRing, message, sendMailboxMessageListener),
|
||||
() -> {
|
||||
log.info("We cannot authenticate to peer. Peer might be offline. We will store message in mailbox.");
|
||||
trySendEncryptedMailboxMessage(peerAddress, peersPubKeyRing, message, sendMailboxMessageListener);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void trySendEncryptedMailboxMessage(Address peerAddress, PubKeyRing peersPubKeyRing, MailboxMessage message, SendMailboxMessageListener sendMailboxMessageListener) {
|
||||
try {
|
||||
SealedAndSignedMessage sealedAndSignedMessage = encryptionService.encryptAndSignMessage(peersPubKeyRing, message);
|
||||
SettableFuture<Connection> future = sendMessage(peerAddress, sealedAndSignedMessage);
|
||||
Futures.addCallback(future, new FutureCallback<Connection>() {
|
||||
@Override
|
||||
public void onSuccess(@Nullable Connection connection) {
|
||||
log.trace("SendEncryptedMailboxMessage onSuccess");
|
||||
UserThread.execute(() -> sendMailboxMessageListener.onArrived());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable throwable) {
|
||||
log.trace("SendEncryptedMailboxMessage onFailure");
|
||||
log.debug(throwable.toString());
|
||||
log.info("We cannot send message to peer. Peer might be offline. We will store message in mailbox.");
|
||||
log.trace("create MailboxEntry with peerAddress " + peerAddress);
|
||||
PublicKey receiverStoragePublicKey = peersPubKeyRing.getStorageSignaturePubKey();
|
||||
addMailboxData(new ExpirableMailboxPayload(sealedAndSignedMessage,
|
||||
keyRing.getStorageSignatureKeyPair().getPublic(),
|
||||
receiverStoragePublicKey),
|
||||
receiverStoragePublicKey);
|
||||
UserThread.execute(() -> sendMailboxMessageListener.onStoredInMailbox());
|
||||
}
|
||||
});
|
||||
} catch (CryptoException e) {
|
||||
e.printStackTrace();
|
||||
log.error("sendEncryptedMessage failed");
|
||||
UserThread.execute(() -> sendMailboxMessageListener.onFault());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// ProtectedData
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public boolean addData(ExpirablePayload expirablePayload) {
|
||||
if (!authenticatedToFirstPeer)
|
||||
throw new AuthenticationException("You must be authenticated before adding data to the P2P network.");
|
||||
|
||||
try {
|
||||
return dataStorage.add(dataStorage.getDataWithSignedSeqNr(expirablePayload, keyRing.getStorageSignatureKeyPair()), networkNode.getAddress());
|
||||
} catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException e) {
|
||||
log.error("Signing at getDataWithSignedSeqNr failed. That should never happen.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean addMailboxData(ExpirableMailboxPayload expirableMailboxPayload, PublicKey receiversPublicKey) {
|
||||
if (!authenticatedToFirstPeer)
|
||||
throw new AuthenticationException("You must be authenticated before adding data to the P2P network.");
|
||||
|
||||
try {
|
||||
return dataStorage.add(dataStorage.getMailboxDataWithSignedSeqNr(expirableMailboxPayload, keyRing.getStorageSignatureKeyPair(), receiversPublicKey), networkNode.getAddress());
|
||||
} catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException e) {
|
||||
log.error("Signing at getDataWithSignedSeqNr failed. That should never happen.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean removeData(ExpirablePayload expirablePayload) {
|
||||
if (!authenticatedToFirstPeer)
|
||||
throw new AuthenticationException("You must be authenticated before removing data from the P2P network.");
|
||||
try {
|
||||
return dataStorage.remove(dataStorage.getDataWithSignedSeqNr(expirablePayload, keyRing.getStorageSignatureKeyPair()), networkNode.getAddress());
|
||||
} catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException e) {
|
||||
log.error("Signing at getDataWithSignedSeqNr failed. That should never happen.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean removeMailboxData(ExpirableMailboxPayload expirableMailboxPayload, PublicKey receiversPublicKey) {
|
||||
if (!authenticatedToFirstPeer)
|
||||
throw new AuthenticationException("You must be authenticated before removing data from the P2P network.");
|
||||
try {
|
||||
return dataStorage.removeMailboxData(dataStorage.getMailboxDataWithSignedSeqNr(expirableMailboxPayload, keyRing.getStorageSignatureKeyPair(), receiversPublicKey), networkNode.getAddress());
|
||||
} catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException e) {
|
||||
log.error("Signing at getDataWithSignedSeqNr failed. That should never happen.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public Map<BigInteger, ProtectedData> getDataMap() {
|
||||
return dataStorage.getMap();
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Listeners
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public void addMessageListener(MessageListener messageListener) {
|
||||
networkNode.addMessageListener(messageListener);
|
||||
}
|
||||
|
||||
public void removeMessageListener(MessageListener messageListener) {
|
||||
networkNode.removeMessageListener(messageListener);
|
||||
}
|
||||
|
||||
public void addDecryptedMailListener(DecryptedMailListener listener) {
|
||||
decryptedMailListeners.add(listener);
|
||||
}
|
||||
|
||||
public void removeDecryptedMailListener(DecryptedMailListener listener) {
|
||||
decryptedMailListeners.remove(listener);
|
||||
}
|
||||
|
||||
public void addDecryptedMailboxListener(DecryptedMailboxListener listener) {
|
||||
decryptedMailboxListeners.add(listener);
|
||||
}
|
||||
|
||||
public void removeDecryptedMailboxListener(DecryptedMailboxListener listener) {
|
||||
decryptedMailboxListeners.remove(listener);
|
||||
}
|
||||
|
||||
public void addP2PServiceListener(P2PServiceListener listener) {
|
||||
p2pServiceListeners.add(listener);
|
||||
}
|
||||
|
||||
public void removeP2PServiceListener(P2PServiceListener listener) {
|
||||
p2pServiceListeners.remove(listener);
|
||||
}
|
||||
|
||||
public void addHashSetChangedListener(HashSetChangedListener hashSetChangedListener) {
|
||||
dataStorage.addHashSetChangedListener(hashSetChangedListener);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Getters
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public NetworkNode getNetworkNode() {
|
||||
return networkNode;
|
||||
}
|
||||
|
||||
public Routing getRouting() {
|
||||
return routing;
|
||||
}
|
||||
|
||||
public Address getAddress() {
|
||||
return networkNode.getAddress();
|
||||
}
|
||||
|
||||
public NetworkStatistics getNetworkStatistics() {
|
||||
return networkStatistics;
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Private
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private void sendGetAllDataMessage(List<Address> seedNodeAddresses) {
|
||||
Address networkNodeAddress = networkNode.getAddress();
|
||||
if (networkNodeAddress != null)
|
||||
seedNodeAddresses.remove(networkNodeAddress);
|
||||
List<Address> remainingSeedNodeAddresses = new CopyOnWriteArrayList<>(seedNodeAddresses);
|
||||
|
||||
if (!seedNodeAddresses.isEmpty()) {
|
||||
Collections.shuffle(remainingSeedNodeAddresses);
|
||||
Address candidate = remainingSeedNodeAddresses.remove(0);
|
||||
log.info("We try to send a GetAllDataMessage request to a random seed node. " + candidate);
|
||||
|
||||
// we use a nonce to see if we are sending to ourselves in case we are a seed node
|
||||
// we don't know our own onion address at that moment so we cannot filter seed nodes
|
||||
SettableFuture<Connection> future = sendMessage(candidate, new GetDataSetMessage(addToListAndGetNonce()));
|
||||
Futures.addCallback(future, new FutureCallback<Connection>() {
|
||||
@Override
|
||||
public void onSuccess(@Nullable Connection connection) {
|
||||
log.info("Send GetAllDataMessage to " + candidate + " succeeded.");
|
||||
connectedSeedNodes.add(candidate);
|
||||
|
||||
// we try to connect to 2 seed nodes
|
||||
if (connectedSeedNodes.size() < 2 && !remainingSeedNodeAddresses.isEmpty()) {
|
||||
// give a random pause of 1-3 sec. before using the next
|
||||
sendGetAllDataMessageTimer = new Timer();
|
||||
sendGetAllDataMessageTimer.schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
sendGetAllDataMessage(remainingSeedNodeAddresses);
|
||||
}
|
||||
}, new Random().nextInt(2000) + 1000);
|
||||
} else {
|
||||
allSeedNodesRequested = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable throwable) {
|
||||
log.info("Send GetAllDataMessage to " + candidate + " failed. Exception:" + throwable.getMessage());
|
||||
log.trace("We try to connect another random seed node. " + remainingSeedNodeAddresses);
|
||||
sendGetAllDataMessage(remainingSeedNodeAddresses);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
log.info("There is no seed node available for requesting data. That is expected for the first seed node.");
|
||||
dataReceived();
|
||||
allSeedNodesRequested = true;
|
||||
}
|
||||
}
|
||||
|
||||
private long addToListAndGetNonce() {
|
||||
long nonce = new Random().nextLong();
|
||||
while (nonce == 0) {
|
||||
nonce = new Random().nextLong();
|
||||
}
|
||||
getDataSetMessageNonceList.add(nonce);
|
||||
return nonce;
|
||||
}
|
||||
|
||||
private void dataReceived() {
|
||||
if (!allDataReceived) {
|
||||
allDataReceived = true;
|
||||
UserThread.execute(() -> p2pServiceListeners.stream().forEach(e -> e.onAllDataReceived()));
|
||||
|
||||
tryStartAuthentication();
|
||||
}
|
||||
}
|
||||
|
||||
private void tryStartAuthentication() {
|
||||
// we need to have both the initial data delivered and the hidden service published before we
|
||||
// bootstrap and authenticate to other nodes
|
||||
if (allDataReceived && hiddenServiceReady) {
|
||||
// we remove ourselves in case we are a seed node
|
||||
checkArgument(networkNode.getAddress() != null, "Address must be set when we are authenticated");
|
||||
connectedSeedNodes.remove(networkNode.getAddress());
|
||||
|
||||
routing.startAuthentication(connectedSeedNodes);
|
||||
}
|
||||
}
|
||||
|
||||
private SettableFuture<Connection> sendMessage(Address peerAddress, Message message) {
|
||||
return networkNode.sendMessage(peerAddress, message);
|
||||
}
|
||||
|
||||
private HashSet<ProtectedData> getHashSet() {
|
||||
return new HashSet<>(dataStorage.getMap().values());
|
||||
}
|
||||
|
||||
private void tryDecryptMailboxData(ProtectedMailboxData mailboxData) {
|
||||
ExpirablePayload data = mailboxData.expirablePayload;
|
||||
if (data instanceof ExpirableMailboxPayload) {
|
||||
ExpirableMailboxPayload mailboxEntry = (ExpirableMailboxPayload) data;
|
||||
SealedAndSignedMessage sealedAndSignedMessage = mailboxEntry.sealedAndSignedMessage;
|
||||
try {
|
||||
DecryptedMessageWithPubKey decryptedMessageWithPubKey = encryptionService.decryptAndVerifyMessage(sealedAndSignedMessage);
|
||||
if (decryptedMessageWithPubKey.message instanceof MailboxMessage) {
|
||||
MailboxMessage mailboxMessage = (MailboxMessage) decryptedMessageWithPubKey.message;
|
||||
Address senderAddress = mailboxMessage.getSenderAddress();
|
||||
checkNotNull(senderAddress, "senderAddress must not be null for mailbox messages");
|
||||
|
||||
log.trace("mailboxData.publicKey " + mailboxData.ownerStoragePubKey.hashCode());
|
||||
log.trace("keyRing.getStorageSignatureKeyPair().getPublic() " + keyRing.getStorageSignatureKeyPair().getPublic().hashCode());
|
||||
log.trace("keyRing.getMsgSignatureKeyPair().getPublic() " + keyRing.getMsgSignatureKeyPair().getPublic().hashCode());
|
||||
log.trace("keyRing.getMsgEncryptionKeyPair().getPublic() " + keyRing.getMsgEncryptionKeyPair().getPublic().hashCode());
|
||||
|
||||
|
||||
mailboxMap.put(decryptedMessageWithPubKey, mailboxData);
|
||||
log.trace("Decryption of SealedAndSignedMessage succeeded. senderAddress=" + senderAddress + " / my address=" + getAddress());
|
||||
UserThread.execute(() -> decryptedMailboxListeners.stream().forEach(e -> e.onMailboxMessageAdded(decryptedMessageWithPubKey, senderAddress)));
|
||||
}
|
||||
} catch (CryptoException e) {
|
||||
log.trace("Decryption of SealedAndSignedMessage failed. That is expected if the message is not intended for us.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* This file is part of Bitsquare.
|
||||
*
|
||||
* Bitsquare is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bitsquare is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bitsquare. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package io.bitsquare.crypto;
|
||||
|
||||
|
||||
import io.bitsquare.common.crypto.CryptoException;
|
||||
import io.bitsquare.common.crypto.KeyRing;
|
||||
import io.bitsquare.common.crypto.KeyStorage;
|
||||
import io.bitsquare.common.crypto.PubKeyRing;
|
||||
import io.bitsquare.common.util.Utilities;
|
||||
import io.bitsquare.p2p.Address;
|
||||
import io.bitsquare.p2p.messaging.DecryptedMessageWithPubKey;
|
||||
import io.bitsquare.p2p.messaging.MailboxMessage;
|
||||
import io.bitsquare.p2p.messaging.SealedAndSignedMessage;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.CertificateException;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class EncryptionServiceTests {
|
||||
private static final Logger log = LoggerFactory.getLogger(EncryptionServiceTests.class);
|
||||
|
||||
@Rule
|
||||
public ExpectedException thrown = ExpectedException.none();
|
||||
|
||||
private PubKeyRing pubKeyRing;
|
||||
private KeyRing keyRing;
|
||||
private File dir = new File("/tmp/bitsquare_tests");
|
||||
|
||||
@Before
|
||||
public void setup() throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, CryptoException {
|
||||
dir.mkdir();
|
||||
KeyStorage keyStorage = new KeyStorage(dir);
|
||||
keyRing = new KeyRing(keyStorage);
|
||||
pubKeyRing = keyRing.getPubKeyRing();
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws IOException {
|
||||
Utilities.deleteDirectory(dir);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDecryptAndVerifyMessage() throws CryptoException {
|
||||
EncryptionService encryptionService = new EncryptionService(keyRing);
|
||||
TestMessage data = new TestMessage("test");
|
||||
SealedAndSignedMessage encrypted = encryptionService.encryptAndSignMessage(pubKeyRing, data);
|
||||
DecryptedMessageWithPubKey decrypted = encryptionService.decryptAndVerifyMessage(encrypted);
|
||||
assertEquals(data.data, ((TestMessage) decrypted.message).data);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class TestMessage implements MailboxMessage {
|
||||
public String data = "test";
|
||||
|
||||
public TestMessage(String data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Address getSenderAddress() {
|
||||
return null;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue