package com.door43.translationstudio.service;
import android.app.Service;
import android.content.Context;
import android.net.DhcpInfo;
import android.net.wifi.WifiManager;
import android.util.Base64;
import com.door43.tools.reporting.Logger;
import com.door43.translationstudio.network.Connection;
import com.door43.translationstudio.network.Peer;
import com.door43.util.RSAEncryption;
import com.tozny.crypto.android.AesCbcWithIntegrity;
import java.io.IOException;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static com.tozny.crypto.android.AesCbcWithIntegrity.decryptString;
import static com.tozny.crypto.android.AesCbcWithIntegrity.encrypt;
import static com.tozny.crypto.android.AesCbcWithIntegrity.generateKey;
import static com.tozny.crypto.android.AesCbcWithIntegrity.keyString;
import static com.tozny.crypto.android.AesCbcWithIntegrity.keys;
/**
* Created by joel on 7/23/2015.
*/
public abstract class NetworkService extends Service {
private static int CONNECTION_TIMEOUT = 30000; // 30 seconds
private Map<String, Peer> mPeers = new HashMap<String, Peer>();
/**
* Returns the broadcast ip address
* @return
* @throws IOException
*/
public InetAddress getBroadcastAddress() throws UnknownHostException {
WifiManager wifi = (WifiManager)getApplication().getSystemService(Context.WIFI_SERVICE);
DhcpInfo dhcp = wifi.getDhcpInfo();
if(dhcp == null) {
throw new UnknownHostException();
}
int broadcast = (dhcp.ipAddress & dhcp.netmask) | ~dhcp.netmask;
byte[] quads = new byte[4];
for (int k = 0; k < 4; k++) {
quads[k] = (byte) ((broadcast >> k * 8) & 0xFF);
}
return InetAddress.getByAddress(quads);
}
/**
* Returns the ip address of the device
* @return
*/
public static String getIpAddress() {
try {
for (Enumeration en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements();) {
NetworkInterface intf = (NetworkInterface)en.nextElement();
for (Enumeration enumIpAddr = intf.getInetAddresses(); enumIpAddr.hasMoreElements();) {
InetAddress inetAddress = (InetAddress)enumIpAddr.nextElement();
if (!inetAddress.isLoopbackAddress()&&inetAddress instanceof Inet4Address) {
String ipAddress=inetAddress.getHostAddress().toString();
return ipAddress;
}
}
}
} catch (SocketException ex) {
ex.printStackTrace();
}
return null;
}
/**
* Returns a list of ip addresses on the local network
* @param subnet the subnet address. This can be any ip address and the subnet will automatically be retreived.
* @return
*/
public static List<String> checkHosts(String subnet){
List<String> hosts = new ArrayList<String>();
int timeout=1000;
// trim down to just the subnet
String[] pieces = subnet.trim().split("\\.");
if(pieces.length == 4) {
subnet = pieces[0]+"."+pieces[1]+"."+pieces[2];
} else if(pieces.length != 3) {
return hosts;
}
for (int i=1;i<254;i++){
String host=subnet + "." + i;
try {
if (InetAddress.getByName(host).isReachable(timeout)){
hosts.add(host);
Logger.i(NetworkService.class.getName(), host + " is reachable");
}
} catch (IOException e) {
e.printStackTrace();
}
}
return hosts;
}
/**
* Adds a network peer to the list of known peers for this service
* @param p The peer to be added to the list of peers
* @return returns true if the peer is new and was added
*/
protected boolean addPeer(Peer p) {
if(!mPeers.containsKey(p.getIpAddress())) {
mPeers.put(p.getIpAddress(), p);
return true;
} else if(mPeers.get(p.getIpAddress()).getPort() != p.getPort()) {
// the port changed, replace peer
mPeers.put(p.getIpAddress(), p);
return true;
} else {
mPeers.get(p.getIpAddress()).touch();
return false;
}
}
/**
* Removes a peer from the list of peers
* @param p the peer to be removed
*/
protected void removePeer(Peer p) {
if(mPeers.containsKey(p.getIpAddress())) {
mPeers.remove(p.getIpAddress());
}
}
/**
* Returns a list of peers that are connected to this service
* @return
*/
public ArrayList<Peer> getPeers() {
return new ArrayList<Peer>(mPeers.values());
}
/**
* Opens a new temporary socket for transfering a file and lets the client know it should connect to it.
* TODO: I don't think we should attempt to throw too much into the client and server classes.
* They work well at establishing initial contact. We should place this elsewhere.
*/
public ServerSocket openWriteSocket(final OnSocketEventListener listener) {
final ServerSocket serverSocket;
try {
serverSocket = new ServerSocket(0);
serverSocket.setSoTimeout(CONNECTION_TIMEOUT);
} catch (IOException e) {
Logger.e(this.getClass().getName(), "failed to create a sender socket", e);
return null;
}
// begin listening for the socket connection
Thread t = new Thread(new Runnable() {
@Override
public void run() {
Socket socket;
try {
socket = serverSocket.accept();
listener.onOpen(new Connection(socket));
} catch (IOException e) {
Logger.e(this.getClass().getName(), "failed to accept the receiver socket", e);
return;
}
try {
serverSocket.close();
} catch (IOException e) {
Logger.e(this.getClass().getName(), "failed to close the sender socket", e);
}
}
});
t.start();
return serverSocket;
}
/**
* Connects to the end of a data socket
* @param listener
* @return
*/
public void openReadSocket(final Peer peer, final int port, final OnSocketEventListener listener) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
try {
InetAddress serverAddr = InetAddress.getByName(peer.getIpAddress());
Socket socket = new Socket(serverAddr, port);
socket.setSoTimeout(CONNECTION_TIMEOUT);
listener.onOpen(new Connection(socket));
} catch(UnknownHostException e) {
Thread.currentThread().interrupt();
} catch (IOException e) {
Thread.currentThread().interrupt();
}
}
});
t.start();
}
/**
* Encrypts a message with a public key
* @param publicKey the public key that will be used to encrypt the message
* @param message the message to be encrypted
* @return the encrypted message
*/
public String encryptMessage(PublicKey publicKey, String message) {
// TRICKY: RSA is not good for encrypting large amounts of data.
// So we first encrypt the data then encrypt the encryption key using the public key.
// the encrypted key is then attached to the encrypted message.
try {
// encrypt message
AesCbcWithIntegrity.SecretKeys key = generateKey();
AesCbcWithIntegrity.CipherTextIvMac civ = encrypt(message, key);
String encryptedMessage = civ.toString();
// encrypt key
byte[] encryptedKeyBytes = RSAEncryption.encryptData(keyString(key), publicKey);
if(encryptedKeyBytes == null) {
Logger.e(this.getClass().getName(), "Failed to encrypt the message");
return null;
}
// encode key
String encryptedKey = new String(Base64.encode(encryptedKeyBytes, Base64.NO_WRAP));
return encryptedKey + "-key-" + encryptedMessage;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* Decrypts a message using the private key
* @param privateKey
* @param message the message to be decrypted
* @return the decrypted message
*/
public String decryptMessage(PrivateKey privateKey, String message) {
// extract encryption key
try {
String[] pieces = message.split("\\-key\\-");
if (pieces.length == 2) {
// decode key
byte[] data = Base64.decode(pieces[0].getBytes(), Base64.NO_WRAP);
// decrypt key
AesCbcWithIntegrity.SecretKeys key = keys(RSAEncryption.decryptData(data, privateKey));
// decrypt message
AesCbcWithIntegrity.CipherTextIvMac civ = new AesCbcWithIntegrity.CipherTextIvMac(pieces[1]);
return decryptString(civ, key);
} else {
Logger.w(this.getClass().getName(), "Invalid message to decrypt");
return null;
}
} catch(Exception e) {
Logger.e(this.getClass().getName(), "Invalid message to decrypt", e);
return null;
}
}
public interface OnSocketEventListener {
void onOpen(Connection connection);
}
}