579 lines
20 KiB
Java
579 lines
20 KiB
Java
package backone.cloud.call;
|
|
|
|
import android.Manifest;
|
|
import android.app.Activity;
|
|
import android.content.Context;
|
|
import android.content.pm.PackageInfo;
|
|
import android.content.pm.PackageManager;
|
|
import androidx.core.app.ActivityCompat;
|
|
import androidx.core.content.ContextCompat;
|
|
import android.text.TextUtils;
|
|
|
|
import java.io.ByteArrayOutputStream;
|
|
import java.io.File;
|
|
import java.io.FileInputStream;
|
|
import java.io.FileOutputStream;
|
|
import java.io.IOException;
|
|
import java.net.Inet6Address;
|
|
import java.net.InetAddress;
|
|
import java.net.InetSocketAddress;
|
|
import java.net.InterfaceAddress;
|
|
import java.net.NetworkInterface;
|
|
import java.net.UnknownHostException;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.Collections;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
import java.util.regex.Pattern;
|
|
|
|
|
|
class Utils {
|
|
|
|
public static boolean hasReadPermission(Activity activity) {
|
|
return (ContextCompat.checkSelfPermission(
|
|
activity, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED);
|
|
}
|
|
|
|
public static boolean hasWritePermission(Activity activity) {
|
|
return (ContextCompat.checkSelfPermission(
|
|
activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED);
|
|
}
|
|
|
|
public static boolean hasCameraPermission(Activity activity) {
|
|
return (ContextCompat.checkSelfPermission(
|
|
activity, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED);
|
|
}
|
|
|
|
public static boolean hasMicPermission(Activity activity) {
|
|
return ContextCompat.checkSelfPermission(
|
|
activity, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED;
|
|
}
|
|
|
|
public static void requestMicPermission(Activity activity, int requestCode) {
|
|
ActivityCompat.requestPermissions(activity, new String[]{
|
|
Manifest.permission.RECORD_AUDIO}, requestCode);
|
|
}
|
|
|
|
public static void requestCameraPermission(Activity activity, int request_code) {
|
|
ActivityCompat.requestPermissions(activity, new String[]{
|
|
Manifest.permission.CAMERA}, request_code);
|
|
}
|
|
|
|
public static void requestReadPermission(Activity activity, int request_code) {
|
|
ActivityCompat.requestPermissions(activity, new String[]{
|
|
Manifest.permission.READ_EXTERNAL_STORAGE}, request_code);
|
|
}
|
|
|
|
public static void requestWritePermission(Activity activity, int request_code) {
|
|
ActivityCompat.requestPermissions(activity, new String[]{
|
|
Manifest.permission.WRITE_EXTERNAL_STORAGE}, request_code);
|
|
}
|
|
|
|
public static boolean allGranted(int[] grantResults) {
|
|
if (grantResults == null || grantResults.length == 0) {
|
|
return false;
|
|
}
|
|
for (int result : grantResults) {
|
|
if (result != PackageManager.PERMISSION_GRANTED) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public static String getApplicationVersion(Context context) {
|
|
try {
|
|
PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
|
|
return info.versionName;
|
|
} catch (PackageManager.NameNotFoundException e) {
|
|
e.printStackTrace();
|
|
}
|
|
return "";
|
|
}
|
|
|
|
public static String join(List<String> list) {
|
|
return TextUtils.join(", ", list);
|
|
}
|
|
|
|
public static List<String> split(String str) {
|
|
List<String> out = new ArrayList<>();
|
|
if (str == null) {
|
|
return out;
|
|
}
|
|
String[] parts = str.split("\\s*,\\s*");
|
|
for (String part : parts) {
|
|
if (part == null) {
|
|
continue;
|
|
}
|
|
String trimmed = part.trim();
|
|
if (!trimmed.isEmpty()) {
|
|
out.add(trimmed);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
private static final Pattern NAME_PATTERN = Pattern.compile("[\\w _-]+");
|
|
|
|
// check for a name that has no funny unicode characters to not let them look to much like other names
|
|
public static boolean isValidName(String name) {
|
|
if (name == null || name.length() == 0) {
|
|
return false;
|
|
}
|
|
|
|
if (!name.equals(name.trim())) {
|
|
return false;
|
|
}
|
|
|
|
return NAME_PATTERN.matcher(name).matches();
|
|
}
|
|
|
|
/** Default display name: "Ordal" plus 4 random alphanumeric characters. */
|
|
public static String generateDefaultUsername() {
|
|
final String alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
java.security.SecureRandom random = new java.security.SecureRandom();
|
|
StringBuilder suffix = new StringBuilder(4);
|
|
for (int i = 0; i < 4; i += 1) {
|
|
suffix.append(alphabet.charAt(random.nextInt(alphabet.length())));
|
|
}
|
|
return "Ordal" + suffix;
|
|
}
|
|
|
|
private final static char[] hexArray = "0123456789abcdef".toCharArray();
|
|
|
|
public static String byteArrayToHexString(byte[] bytes) {
|
|
if (bytes == null) {
|
|
return "";
|
|
}
|
|
char[] hexChars = new char[bytes.length * 2];
|
|
for (int j = 0; j < bytes.length; j += 1) {
|
|
int v = bytes[j] & 0xFF;
|
|
hexChars[j * 2] = hexArray[v >>> 4];
|
|
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
|
|
}
|
|
return new String(hexChars);
|
|
}
|
|
|
|
public static byte[] hexStringToByteArray(String str) {
|
|
if (str == null || str.length() == 0 || (str.length() % 2) != 0) {
|
|
return null;
|
|
}
|
|
int len = str.length();
|
|
byte[] data = new byte[len / 2];
|
|
for (int i = 0; i < len; i += 2) {
|
|
int hi = Character.digit(str.charAt(i), 16);
|
|
int lo = Character.digit(str.charAt(i + 1), 16);
|
|
if (hi < 0 || lo < 0) {
|
|
return null;
|
|
}
|
|
data[i / 2] = (byte) ((hi << 4) + lo);
|
|
}
|
|
return data;
|
|
}
|
|
|
|
public static InetSocketAddress parseInetSocketAddress(String addr, int defaultPort) {
|
|
if (addr == null || addr.length() == 0) {
|
|
return null;
|
|
}
|
|
try{
|
|
if(isIPv6(addr)){
|
|
return new InetSocketAddress(InetAddress.getByName("["+addr+"]"), defaultPort);
|
|
} else {
|
|
return new InetSocketAddress(InetAddress.getByName(addr), defaultPort);
|
|
}
|
|
} catch (Exception e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public static String bytesToMacAddress(byte[] mac) {
|
|
StringBuilder sb = new StringBuilder();
|
|
for (byte b : mac) {
|
|
sb.append(String.format("%02X:", b));
|
|
}
|
|
|
|
if (sb.length() > 0) {
|
|
sb.deleteCharAt(sb.length() - 1);
|
|
}
|
|
|
|
return sb.toString();
|
|
}
|
|
|
|
public static byte[] macAddressToBytes(String mac) {
|
|
String[] elements = mac.split(":");
|
|
byte[] array = new byte[elements.length];
|
|
|
|
for (int i = 0; i < elements.length; i += 1) {
|
|
array[i] = Integer.decode("0x" + elements[i]).byteValue();
|
|
}
|
|
|
|
return array;
|
|
}
|
|
|
|
// Check if MAC address is unicast/multicast
|
|
public static boolean isMulticastMAC(byte[] mac) {
|
|
return (mac[0] & 1) != 0;
|
|
}
|
|
|
|
// Check if MAC address is local/universal
|
|
public static boolean isUniversalMAC(byte[] mac) {
|
|
return (mac[0] & 2) == 0;
|
|
}
|
|
|
|
public static boolean isValidMAC(byte[] mac) {
|
|
// we ignore the first byte (dummy mac addresses have the "local" bit set - resulting in 0x02)
|
|
return ((mac != null)
|
|
&& (mac.length == 6)
|
|
&& ((mac[1] != 0x0) && (mac[2] != 0x0) && (mac[3] != 0x0) && (mac[4] != 0x0) && (mac[5] != 0x0))
|
|
);
|
|
}
|
|
|
|
private static boolean isHexChar(char c) {
|
|
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
|
|
}
|
|
|
|
// check if a string is a MAC address (heuristic)
|
|
public static boolean isMAC(String address) {
|
|
if (address == null || address.length() != 17) {
|
|
return false;
|
|
}
|
|
|
|
for (int i : new int[]{0, 1, 3, 4, 6, 7, 9, 10, 12, 13, 15, 16}) {
|
|
if (!isHexChar(address.charAt(i))) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
for (int i : new int[]{2, 5, 8, 11, 14}) {
|
|
if (address.charAt(i) != ':') {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static final Pattern DOMAIN_PATTERN = Pattern.compile("[a-z0-9\\-.]+");
|
|
|
|
// check if string is a domain (heuristic)
|
|
public static boolean isDomain(String domain) {
|
|
if (domain == null || domain.length() == 0) {
|
|
return false;
|
|
}
|
|
|
|
if (domain.startsWith(".") || domain.endsWith(".")) {
|
|
return false;
|
|
}
|
|
|
|
if (domain.contains("..") || !domain.contains(".")) {
|
|
return false;
|
|
}
|
|
|
|
if (domain.startsWith("-") || domain.endsWith("-")) {
|
|
return false;
|
|
}
|
|
|
|
if (domain.contains(".-") || domain.contains("-.")) {
|
|
return false;
|
|
}
|
|
|
|
return DOMAIN_PATTERN.matcher(domain).matches();
|
|
}
|
|
|
|
private static final Pattern IPV4_PATTERN = Pattern.compile("^(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}$");
|
|
private static final Pattern IPV6_STD_PATTERN = Pattern.compile("^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$");
|
|
private static final Pattern IPV6_HEX_COMPRESSED_PATTERN = Pattern.compile("^((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)$");
|
|
|
|
// check if a string is an IP address (heuristic)
|
|
public static boolean isIP(String address) {
|
|
return IPV4_PATTERN.matcher(address).matches()
|
|
|| IPV6_STD_PATTERN.matcher(address).matches()
|
|
|| IPV6_HEX_COMPRESSED_PATTERN.matcher(address).matches();
|
|
}
|
|
|
|
public static boolean isIPv6(String address) {
|
|
return IPV6_STD_PATTERN.matcher(address).matches()
|
|
|| IPV6_HEX_COMPRESSED_PATTERN.matcher(address).matches();
|
|
}
|
|
|
|
public static List<AddressEntry> collectAddresses() {
|
|
ArrayList<AddressEntry> addressList = new ArrayList<>();
|
|
try {
|
|
List<NetworkInterface> all = Collections.list(NetworkInterface.getNetworkInterfaces());
|
|
for (NetworkInterface nif : all) {
|
|
String vpn = nif.getName();
|
|
if (nif.isLoopback()) {
|
|
continue;
|
|
}
|
|
|
|
for (InterfaceAddress ia : nif.getInterfaceAddresses()) {
|
|
InetAddress addr = ia.getAddress();
|
|
|
|
if (addr.isLoopbackAddress()) {
|
|
continue;
|
|
}
|
|
|
|
String fullAddress = addr.getHostAddress();
|
|
|
|
if(fullAddress.indexOf('%')>0){
|
|
String subaddress = fullAddress.substring(0, fullAddress.indexOf('%'));
|
|
if(isIPv6(subaddress)){
|
|
subaddress= subaddress;
|
|
|
|
}
|
|
if ((vpn.contains("tun"))) {
|
|
addressList.add(new AddressEntry(subaddress, nif.getName(), addr.isMulticastAddress()));
|
|
}
|
|
} else {
|
|
if(isIPv6(fullAddress)){
|
|
fullAddress= fullAddress;
|
|
}
|
|
if ((vpn.contains("tun"))) {
|
|
addressList.add(new AddressEntry(fullAddress, nif.getName(), addr.isMulticastAddress()));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (Exception ex) {
|
|
// ignore
|
|
log("error: " + ex);
|
|
}
|
|
|
|
return addressList;
|
|
}
|
|
|
|
/** Verified Bachot identity: public_key is the 4-byte TUN IPv4. */
|
|
public static boolean isIpv4IdentityKey(byte[] key) {
|
|
return key != null && key.length == 4;
|
|
}
|
|
|
|
public static String ipv4BytesToString(byte[] bytes) {
|
|
if (!isIpv4IdentityKey(bytes)) {
|
|
return null;
|
|
}
|
|
return String.format(Locale.US, "%d.%d.%d.%d",
|
|
bytes[0] & 0xFF, bytes[1] & 0xFF, bytes[2] & 0xFF, bytes[3] & 0xFF);
|
|
}
|
|
|
|
public static byte[] ipv4StringToBytes(String ipv4) {
|
|
if (ipv4 == null || !IPV4_PATTERN.matcher(ipv4).matches()) {
|
|
return null;
|
|
}
|
|
try {
|
|
byte[] bytes = InetAddress.getByName(ipv4).getAddress();
|
|
if (bytes != null && bytes.length == 4) {
|
|
return bytes;
|
|
}
|
|
} catch (Exception e) {
|
|
// ignore
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** Prefer tun0 IPv4 for verified identity; fall back to any other tun* IPv4. */
|
|
public static String getFirstTunIpv4() {
|
|
String fallback = null;
|
|
for (AddressEntry entry : collectAddresses()) {
|
|
if (entry == null || entry.address == null || entry.multicast || entry.device == null) {
|
|
continue;
|
|
}
|
|
if (!isIP(entry.address) || isIPv6(entry.address)) {
|
|
continue;
|
|
}
|
|
if ("tun0".equals(entry.device)) {
|
|
return entry.address;
|
|
}
|
|
if (fallback == null && entry.device.contains("tun")) {
|
|
fallback = entry.address;
|
|
}
|
|
}
|
|
return fallback;
|
|
}
|
|
|
|
/**
|
|
* Set own public_key + addresses from tun0 IPv4 so QR/dial match verified builds.
|
|
* @return true if settings were changed
|
|
*/
|
|
public static boolean syncIdentityFromTun(Settings settings) {
|
|
if (settings == null) {
|
|
return false;
|
|
}
|
|
String ipv4 = getFirstTunIpv4();
|
|
if (ipv4 == null) {
|
|
return false;
|
|
}
|
|
byte[] identity = ipv4StringToBytes(ipv4);
|
|
if (identity == null) {
|
|
return false;
|
|
}
|
|
|
|
boolean changed = false;
|
|
if (!Arrays.equals(settings.getPublicKey(), identity)) {
|
|
settings.setPublicKey(identity);
|
|
// IP-as-key peers use legacy plaintext signaling (no ed25519 secret).
|
|
settings.setSecretKey(null);
|
|
changed = true;
|
|
}
|
|
|
|
// Keep dial/QR address as the tun0 IPv4 only (verified ID).
|
|
List<String> addresses = settings.getAddresses();
|
|
if (addresses.size() != 1 || !ipv4.equalsIgnoreCase(addresses.get(0))) {
|
|
addresses.clear();
|
|
addresses.add(ipv4);
|
|
changed = true;
|
|
}
|
|
return changed;
|
|
}
|
|
|
|
// list all IP/MAC addresses of running network interfaces - for debugging only
|
|
public static void printOwnAddresses() {
|
|
for (AddressEntry ae : collectAddresses()) {
|
|
log("Address: " + ae.address + " (" + ae.device + (ae.multicast ? ", multicast" : "") + ")");
|
|
}
|
|
}
|
|
|
|
// Check if the given MAC address is in the IPv6 address
|
|
public static byte[] getEUI64MAC(Inet6Address addr6) {
|
|
byte[] bytes = addr6.getAddress();
|
|
if (bytes[11] != ((byte) 0xFF) || bytes[12] != ((byte) 0xFE)) {
|
|
return null;
|
|
}
|
|
|
|
byte[] mac = new byte[6];
|
|
mac[0] = (byte) (bytes[8] ^ 2);
|
|
mac[1] = bytes[9];
|
|
mac[2] = bytes[10];
|
|
mac[3] = bytes[13];
|
|
mac[4] = bytes[14];
|
|
mac[5] = bytes[15];
|
|
return mac;
|
|
}
|
|
|
|
/*
|
|
* Replace the MAC address of an EUi64 scheme IPv6 address with another MAC address.
|
|
* E.g.: ("fe80::aaaa:aaff:faa:aaa", "bb:bb:bb:bb:bb:bb") => "fe80::9bbb:bbff:febb:bbbb"
|
|
*/
|
|
private static Inet6Address createEUI64Address(Inet6Address addr6, byte[] mac) {
|
|
// addr6 is expected to be a EUI64 address
|
|
try {
|
|
byte[] bytes = addr6.getAddress();
|
|
|
|
bytes[8] = (byte) (mac[0] ^ 2);
|
|
bytes[9] = mac[1];
|
|
bytes[10] = mac[2];
|
|
|
|
// already set, but doesn't harm
|
|
bytes[11] = (byte) 0xFF;
|
|
bytes[12] = (byte) 0xFE;
|
|
|
|
bytes[13] = mac[3];
|
|
bytes[14] = mac[4];
|
|
bytes[15] = mac[5];
|
|
|
|
return Inet6Address.getByAddress(null, bytes, addr6.getScopeId());
|
|
} catch (UnknownHostException e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Iterate all device addresses, check if they conform to the EUI64 scheme.
|
|
* If yes, replace the MAC address in it with the supplied one and return that address.
|
|
* Also set the given port for those generated addresses.
|
|
*/
|
|
public static List<InetSocketAddress> getAddressPermutations(String contact_mac, int port) {
|
|
byte[] contact_mac_bytes = Utils.macAddressToBytes(contact_mac);
|
|
ArrayList<InetSocketAddress> addrs = new ArrayList<InetSocketAddress>();
|
|
try {
|
|
List<NetworkInterface> all = Collections.list(NetworkInterface.getNetworkInterfaces());
|
|
for (NetworkInterface nif : all) {
|
|
if (nif.isLoopback()) {
|
|
continue;
|
|
}
|
|
|
|
for (InterfaceAddress ia : nif.getInterfaceAddresses()) {
|
|
InetAddress addr = ia.getAddress();
|
|
if (addr.isLoopbackAddress()) {
|
|
continue;
|
|
}
|
|
|
|
if (addr instanceof Inet6Address) {
|
|
Inet6Address addr6 = (Inet6Address) addr;
|
|
byte[] extracted_mac = getEUI64MAC(addr6);
|
|
if (extracted_mac != null && Arrays.equals(extracted_mac, nif.getHardwareAddress())) {
|
|
// We found the interface MAC address in the IPv6 assigned to that interface in the EUI-64 scheme.
|
|
// Now assume that the contact has an address with the same scheme.
|
|
InetAddress new_addr = createEUI64Address(addr6, contact_mac_bytes);
|
|
if (new_addr != null) {
|
|
addrs.add(new InetSocketAddress(new_addr, port));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (Exception e) {
|
|
e.printStackTrace();
|
|
}
|
|
|
|
return addrs;
|
|
}
|
|
|
|
// EUI-64 based address to MAC address
|
|
public static String getGeneralizedAddress(InetAddress address) {
|
|
if (address == null) {
|
|
return "";
|
|
}
|
|
if (address instanceof Inet6Address) {
|
|
// if the IPv6 address contains a MAC address, take that.
|
|
byte[] mac = Utils.getEUI64MAC((Inet6Address) address);
|
|
if (mac != null) {
|
|
return Utils.bytesToMacAddress(mac);
|
|
}
|
|
}
|
|
String host = address.getHostAddress();
|
|
return host != null ? host : "";
|
|
}
|
|
|
|
// write file to external storage
|
|
public static void writeExternalFile(String filepath, byte[] data) throws IOException {
|
|
File file = new File(filepath);
|
|
|
|
if (file.exists() && file.isFile()) {
|
|
if (!file.delete()) {
|
|
throw new IOException("Failed to delete existing file: " + filepath);
|
|
}
|
|
}
|
|
|
|
file.createNewFile();
|
|
FileOutputStream fos = new FileOutputStream(file);
|
|
fos.write(data);
|
|
fos.close();
|
|
}
|
|
|
|
// read file from external storage
|
|
public static byte[] readExternalFile(String filepath) throws IOException {
|
|
File file = new File(filepath);
|
|
|
|
if (!file.exists() || !file.isFile()) {
|
|
throw new IOException("File does not exist: " + filepath);
|
|
}
|
|
|
|
FileInputStream fis = new FileInputStream(file);
|
|
|
|
int nRead;
|
|
byte[] data = new byte[16384];
|
|
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
|
while ((nRead = fis.read(data, 0, data.length)) != -1) {
|
|
buffer.write(data, 0, nRead);
|
|
}
|
|
|
|
fis.close();
|
|
return buffer.toByteArray();
|
|
}
|
|
|
|
private static void log(String s) {
|
|
Log.d(Utils.class.getSimpleName(), s);
|
|
}
|
|
}
|