All Downloads are FREE. Search and download functionalities are using the official Maven repository.

it.auties.whatsapp.registration.TokenProvider Maven / Gradle / Ivy

package it.auties.whatsapp.registration;

import it.auties.curve25519.Curve25519;
import it.auties.whatsapp.controller.Keys;
import it.auties.whatsapp.crypto.MD5;
import it.auties.whatsapp.crypto.Sha256;
import it.auties.whatsapp.model.business.BusinessVerifiedNameCertificateBuilder;
import it.auties.whatsapp.model.business.BusinessVerifiedNameCertificateSpec;
import it.auties.whatsapp.model.business.BusinessVerifiedNameDetailsBuilder;
import it.auties.whatsapp.model.business.BusinessVerifiedNameDetailsSpec;
import it.auties.whatsapp.model.response.WebVersionResponse;
import it.auties.whatsapp.model.signal.auth.UserAgent;
import it.auties.whatsapp.model.signal.auth.UserAgent.PlatformType;
import it.auties.whatsapp.model.signal.auth.Version;
import it.auties.whatsapp.util.BytesHelper;
import it.auties.whatsapp.util.Json;
import it.auties.whatsapp.util.Medias;
import it.auties.whatsapp.util.Specification.Whatsapp;
import net.dongliu.apk.parser.ByteArrayApkFile;
import net.dongliu.apk.parser.bean.ApkSigner;
import net.dongliu.apk.parser.bean.CertificateMeta;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;
import java.security.Security;
import java.security.cert.CertificateException;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.concurrent.CompletableFuture;

import static java.net.http.HttpResponse.BodyHandlers.ofString;

public final class TokenProvider {
    static {
        Security.addProvider(new BouncyCastleProvider());
    }

    private static volatile Version webVersion;
    private static volatile Version iosVersion;
    private static volatile WhatsappApk cachedApk;
    private static volatile WhatsappApk cachedBusinessApk;

    private static Path androidCache = Path.of(System.getProperty("user.home") + "/.cobalt/token/android");

    public static void setAndroidCache(Path path) {
        try {
            Files.createDirectories(path);
            androidCache = path;
        } catch (IOException exception) {
            throw new UncheckedIOException(exception);
        }
    }

    public static CompletableFuture getVersion(UserAgent.PlatformType platform) {
        return getVersion(platform, true);
    }

    private static CompletableFuture getVersion(UserAgent.PlatformType platform, boolean useJarCache) {
        return switch (platform) {
            case WEB, WINDOWS, MACOS ->
                    getWebVersion();
            case ANDROID, ANDROID_BUSINESS ->
                    getAndroidData(platform.isBusiness(), useJarCache).thenApply(WhatsappApk::version);
            case IOS, IOS_BUSINESS ->
                    CompletableFuture.completedFuture(platform.isBusiness() ? Whatsapp.DEFAULT_MOBILE_BUSINESS_IOS_VERSION : Whatsapp.DEFAULT_MOBILE_IOS_VERSION); // Fetching the latest ios version is harder than one might hope
            case KAIOS ->
                CompletableFuture.completedFuture(Whatsapp.DEFAULT_MOBILE_KAIOS_VERSION);
            default -> throw new IllegalStateException("Unsupported mobile os: " + platform);
        };
    }

    private static CompletableFuture getWebVersion() {
        try (var client = HttpClient.newHttpClient()) {
            if (webVersion != null) {
                return CompletableFuture.completedFuture(webVersion);
            }

            var request = HttpRequest.newBuilder()
                    .GET()
                    .uri(URI.create(Whatsapp.WEB_UPDATE_URL))
                    .build();
            return client.sendAsync(request, ofString())
                    .thenApplyAsync(response -> Json.readValue(response.body(), WebVersionResponse.class))
                    .thenApplyAsync(version -> webVersion = Version.of(version.currentVersion()));
        } catch (Throwable throwable) {
            throw new RuntimeException("Cannot fetch latest web version", throwable);
        }
    }

    public static CompletableFuture getToken(long phoneNumber, PlatformType platform, boolean useJarCache) {
        return switch (platform) {
            case ANDROID, ANDROID_BUSINESS -> getAndroidToken(String.valueOf(phoneNumber), platform.isBusiness(), useJarCache);
            case IOS, IOS_BUSINESS -> getIosToken(phoneNumber, platform, useJarCache);
            case KAIOS -> getKaiOsToken(phoneNumber, platform, useJarCache);
            default -> throw new IllegalStateException("Unsupported mobile os: " + platform);
        };
    }

    private static CompletableFuture getIosToken(long phoneNumber, UserAgent.PlatformType platform, boolean useJarCache) {
        return getVersion(platform, useJarCache)
                .thenApply(version -> getIosToken(phoneNumber, version, platform.isBusiness()));
    }

    private static String getIosToken(long phoneNumber, Version version, boolean business) {
        var staticToken = business ? Whatsapp.MOBILE_BUSINESS_IOS_STATIC : Whatsapp.MOBILE_IOS_STATIC;
        var token = staticToken + HexFormat.of().formatHex(version.toHash()) + phoneNumber;
        return HexFormat.of().formatHex(MD5.calculate(token));
    }

    private static CompletableFuture getKaiOsToken(long phoneNumber, UserAgent.PlatformType platform, boolean useJarCache) {
        return getVersion(platform, useJarCache)
                .thenApply(version -> getKaiOsToken(phoneNumber, version));
    }

    private static String getKaiOsToken(long phoneNumber, Version version) {
        var staticTokenPart = HexFormat.of().parseHex(Whatsapp.MOBILE_KAIOS_STATIC);
        var pagePart = HexFormat.of().formatHex(Sha256.calculate(BytesHelper.concat(readKaiOsResource("index.html"), readKaiOsResource("backendRoot.js"))));
        var phonePart = String.valueOf(phoneNumber).getBytes(StandardCharsets.UTF_8);
        return HexFormat.of().formatHex(Sha256.calculate(BytesHelper.concat(staticTokenPart, pagePart.getBytes(StandardCharsets.UTF_8), phonePart)));
    }

    private static byte[] readKaiOsResource(String name) {
        try (var stream = ClassLoader.getSystemResource("token/kaios/" + name).openStream()) {
            return stream.readAllBytes();
        } catch (IOException exception) {
            throw new UncheckedIOException(exception);
        }
    }

    private static CompletableFuture getAndroidToken(String phoneNumber, boolean business, boolean useJarCache) {
        return getAndroidData(business, useJarCache)
                .thenApplyAsync(whatsappData -> getAndroidToken(phoneNumber, whatsappData));
    }

    private static String getAndroidToken(String phoneNumber, WhatsappApk whatsappData) {
        try {
            var mac = Mac.getInstance("HMACSHA1");
            var secretKeyBytes = whatsappData.secretKey();
            var secretKey = new SecretKeySpec(secretKeyBytes, 0, secretKeyBytes.length, "PBKDF2");
            mac.init(secretKey);
            whatsappData.certificates().forEach(mac::update);
            mac.update(whatsappData.md5Hash());
            mac.update(phoneNumber.getBytes(StandardCharsets.UTF_8));
            return URLEncoder.encode(Base64.getEncoder().encodeToString(mac.doFinal()), StandardCharsets.UTF_8);
        } catch (GeneralSecurityException throwable) {
            throw new RuntimeException("Cannot compute mobile token", throwable);
        }
    }

    private static CompletableFuture getAndroidData(boolean business, boolean useJarCache) {
        if (!business && cachedApk != null) {
            return CompletableFuture.completedFuture(cachedApk);
        }

        if (business && cachedBusinessApk != null) {
            return CompletableFuture.completedFuture(cachedBusinessApk);
        }

        return getCachedApk(business, useJarCache)
                .map(CompletableFuture::completedFuture)
                .orElseGet(() -> downloadWhatsappApk(business));
    }

    public static CompletableFuture downloadWhatsappApk(boolean business) {
        return Medias.downloadAsync(business ? Whatsapp.MOBILE_BUSINESS_DOWNLOAD_URL : Whatsapp.MOBILE_DOWNLOAD_URL)
                .thenApplyAsync(result -> getAndroidData(result, business));
    }

    private static Optional getCachedApk(boolean business, boolean useJarCache) {
        try {
            var localCache = getAndroidLocalCache(business);
            if (Files.notExists(localCache)) {
                if (useJarCache) {
                    var jarCache = getAndroidJarCache(business);
                    return Optional.of(Json.readValue(Files.readString(jarCache), WhatsappApk.class));
                }

                return Optional.empty();
            }

            var now = Instant.now();
            var fileTime = Files.getLastModifiedTime(localCache);
            if (fileTime.toInstant().until(now, ChronoUnit.DAYS) > 7) {
                return Optional.empty();
            }

            return Optional.of(Json.readValue(Files.readString(localCache), WhatsappApk.class));
        } catch (Throwable throwable) {
            return Optional.empty();
        }
    }

    private static Path getAndroidJarCache(boolean business) throws URISyntaxException {
        var url = business
                ? ClassLoader.getSystemResource("token/android/whatsapp_business.json")
                : ClassLoader.getSystemResource("token/android/whatsapp.json");
        return Path.of(url.toURI());
    }

    private static Path getAndroidLocalCache(boolean business) {
        return androidCache.resolve(business ? "whatsapp_business.json" : "whatsapp.json");
    }

    private static WhatsappApk getAndroidData(byte[] apk, boolean business) {
        try (var apkFile = new ByteArrayApkFile(apk)) {
            var version = Version.of(apkFile.getApkMeta().getVersionName());
            var md5Hash = MD5.calculate(apkFile.getFileData("classes.dex"));
            var secretKey = getSecretKey(apkFile.getApkMeta().getPackageName(), getAboutLogo(apkFile));
            var certificates = getCertificates(apkFile);
            if (business) {
                var result = new WhatsappApk(version, md5Hash, secretKey.getEncoded(), certificates, true);
                cacheWhatsappData(result);
                return cachedBusinessApk = result;
            }

            var result = new WhatsappApk(version, md5Hash, secretKey.getEncoded(), certificates, false);
            cacheWhatsappData(result);
            return cachedApk = result;
        } catch (IOException | GeneralSecurityException exception) {
            throw new RuntimeException("Cannot extract certificates from APK", exception);
        }
    }

    private static void cacheWhatsappData(WhatsappApk apk) {
        CompletableFuture.runAsync(() -> {
            try {
                var json = Json.writeValueAsString(apk, true);
                var file = getAndroidLocalCache(apk.business());
                Files.createDirectories(file.getParent());
                Files.writeString(file, json);
            } catch (IOException exception) {
                throw new UncheckedIOException(exception);
            }
        });
    }

    private static byte[] getAboutLogo(ByteArrayApkFile apkFile) throws IOException {
        var resource = apkFile.getFileData("res/drawable-hdpi/about_logo.png");
        if (resource != null) {
            return resource;
        }

        var resourceV4 = apkFile.getFileData("res/drawable-hdpi-v4/about_logo.png");
        if (resourceV4 != null) {
            return resourceV4;
        }

        var xxResourceV4 = apkFile.getFileData("res/drawable-xxhdpi-v4/about_logo.png");
        if (xxResourceV4 != null) {
            return xxResourceV4;
        }

        throw new NoSuchElementException("Missing about_logo.png from apk");
    }

    private static List getCertificates(ByteArrayApkFile apkFile) throws IOException, CertificateException {
        return apkFile.getApkSingers()
                .stream()
                .map(ApkSigner::getCertificateMetas)
                .flatMap(Collection::stream)
                .map(CertificateMeta::getData)
                .toList();
    }

    private static SecretKey getSecretKey(String packageName, byte[] resource) throws IOException, GeneralSecurityException {
        var result = BytesHelper.concat(packageName.getBytes(StandardCharsets.UTF_8), resource);
        var whatsappLogoChars = new char[result.length];
        for (var i = 0; i < result.length; i++) {
            whatsappLogoChars[i] = (char) result[i];
        }
        var factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1And8BIT");
        var key = new PBEKeySpec(whatsappLogoChars, Whatsapp.MOBILE_ANDROID_SALT, 128, 512);
        return factory.generateSecret(key);
    }

    public static String generateBusinessCertificate(Keys keys) {
        var details = new BusinessVerifiedNameDetailsBuilder()
                .name("")
                .issuer("smb:wa")
                .serial(Math.abs(new SecureRandom().nextLong()))
                .build();
        var encodedDetails = BusinessVerifiedNameDetailsSpec.encode(details);
        var certificate = new BusinessVerifiedNameCertificateBuilder()
                .encodedDetails(encodedDetails)
                .signature(Curve25519.sign(keys.identityKeyPair().privateKey(), encodedDetails, true))
                .build();
        return Base64.getUrlEncoder().encodeToString(BusinessVerifiedNameCertificateSpec.encode(certificate));
    }

    public record WhatsappApk(Version version, byte[] md5Hash, byte[] secretKey, Collection certificates, boolean business) {

    }

    public static String generateGpiaToken(byte[] deviceIdentifier, int desiredLength) {
        if (deviceIdentifier == null || desiredLength <= 0) {
            throw new IllegalArgumentException();
        }

        var bytesNeeded = (int) Math.ceil((desiredLength * 3) / 4.0);
        var randomBytes = BytesHelper.random(bytesNeeded - deviceIdentifier.length);
        var tokenBytes = new byte[bytesNeeded];
        System.arraycopy(deviceIdentifier, 0, tokenBytes, 0, deviceIdentifier.length);
        System.arraycopy(randomBytes, 0, tokenBytes, deviceIdentifier.length, randomBytes.length);
        var token = Base64.getEncoder().encodeToString(tokenBytes);
        return token.substring(0, desiredLength);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy