
it.auties.whatsapp.registration.TokenProvider Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of cobalt Show documentation
Show all versions of cobalt Show documentation
Standalone fully-featured Whatsapp Web API for Java and Kotlin
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 - 2025 Weber Informatics LLC | Privacy Policy