Skip to content

Instantly share code, notes, and snippets.

@Romern
Last active February 8, 2023 08:11
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save Romern/e58e634e4d70b2be5b57d7abdb77f7ef to your computer and use it in GitHub Desktop.
Save Romern/e58e634e4d70b2be5b57d7abdb77f7ef to your computer and use it in GitHub Desktop.
Goog-Spatula Header Stuff

https://github.com/nikteg/TDA602/blob/f1693d2e00d3929f4b693e3a201754519e4f85ef/project/apps/se.isakpersson.dashyknight/payloads.txt :

CjoKGnNlLmlzYWtwZXJzc29uLmRhc2h5a25pZ2h0GhxMdGh4b0J2OTZ4VW5RYTU0d3Nic1NuaHFBR1k9EiAdDJ1K7i2rbUrFMRD/TrQvB+/XtjGG0gV1BavLZ9rUwRjXqa7Z0aKp3zog7Ne+9szA8at7

https://github.com/4kumano/reftoken/blob/99d1d980c0015c8b1113cb65b02ee0ede96ae471/sumber.txt :

CjoKGmFwcC5nZXRsb2FkZWQuYml0Y29pbmJsYXN0Ghw2Wmk4VHdRTnlpT0QrdXMyNC81YVlwd3h0NUE9GLingOeJmKD6Ng==

https://github.com/abhinfrnd/Jmeter_CI_part_3/blob/5f08774a41dc958ca23f6770a892f1628e83325c/apache-jmeter-3.3/backups/Fourth%20Script-000001.jmx :

CjYKFmNvbS5nb29nbGUuYW5kcm9pZC5nbXMaHE9KR0tSVDBIR1pOVStMR2E4RjdHVml6dFY0Zz0SIKO484a0+56SeVRXXBvWi+tYjQxH4P2r5IxKm/sjfVoYGKK4h7XGq7KUOyCEhZWCtcCImgIqSQBeaoCcLFjUHJx9jxvH64X/Z3KVoy0cEpDaxHsSj/FKRppR4ApV+AF3kwNIhMOPwMsYjz7bdS80if9Q4HRH08eft5Vqiwmg3BU=
/* renamed from: a */
public final String mo13281a(String str) {
bomj bomj;
String concat;
byte[] bArr;
pqy.a(str, "Package name cannot be null!");
if (!((Boolean) gog.f18705as.a()).booleanValue()) {
f19984h.h("DeviceKey is turned off", new Object[0]);
return null;
}
synchronized (this.f19989f) {
bomj a = this.f19986c.mo13283a(); //Reads the device_key in /data/data/com.google.android.gms/files/device_key
if (a == null) {
mo13282a(); //generates a new device key
bomj = this.f19986c.mo13283a();
} else {
bomj = a;
}
if (bomj == null || !((bArr = bomj.d) == null || bArr.length == 0)) {
try {
byte[] d = qdy.d(this.f19985b.f20590c, str);
if (d == null) {
f19984h.e("Unable to get package certificate hash.", new Object[0]);
return null;
}
bbsb bbsb = new bbsb(); //AppCertificate
bbsb.b = str; //appname
bbsb.a = Base64.encodeToString(d, 2); //appcertbase64
bbsc bbsc = new bbsc();
if (bomj != null) {
bbsc.a = bomj.a; //deviceId
bbsc.d = bomj.c; //sessionId
bbsc.c = bomj.b; //
try {
String valueOf = String.valueOf(str);
String valueOf2 = String.valueOf(bbsb.a);
if (valueOf2.length() == 0) {
concat = new String(valueOf);
} else {
concat = valueOf.concat(valueOf2);
}
bbsc.e = m15241a(bomj, concat); //HMACSHA256 of appname+appcertbase64, with key bomj.d
} catch (IOException | IllegalArgumentException e) {
psp psp = f19984h;
String valueOf3 = String.valueOf(e);
StringBuilder sb = new StringBuilder(String.valueOf(valueOf3).length() + 40);
sb.append("Error while creating spatula signature: ");
sb.append(valueOf3);
psp.e(sb.toString(), new Object[0]);
return null;
}
} else {
bbsc.a = qdy.a(this.f19985b.f20590c);
}
bbsc.b = bbsb; //AppCertificate
String encodeToString = Base64.encodeToString(bmnd.toByteArray(bbsc), 2);
return encodeToString;
} catch (PackageManager.NameNotFoundException e2) {
f19984h.b("Invalid package name!", e2, new Object[0]);
return null;
}
} else {
f19984h.e("Invalid device key.", new Object[0]);
return null;
}
}
}
public final boolean mo13282a() { //generates new device_key
FileOutputStream openFileOutput;
if (!((Boolean) gog.f18705as.a()).booleanValue()) {
f19984h.h("DeviceKey is turned off", new Object[0]);
return false;
}
synchronized (this.f19989f) {
long elapsedRealtime = SystemClock.elapsedRealtime();
if (elapsedRealtime - this.f19988e < ((Long) gog.f18652S.a()).longValue() * 1000) {
boolean z = this.f19987d;
return z;
}
this.f19988e = elapsedRealtime; //seed
long nextLong = this.f19990i.nextLong();
long a = qdy.a(this.f19985b.f20590c); //this.f19985b.f20590c is a context
HashMap a2 = besc.a();
a2.put("dg_androidId", Long.toHexString(a));
a2.put("dg_session", Long.toHexString(nextLong));
a2.put("dg_gmsCoreVersion", "13292018");
a2.put("dg_sdkVersion", String.valueOf(Build.VERSION.SDK_INT));
String a3 = gmh.m13852a(this.f19985b.f20590c, "devicekey", (Map) a2); //something droidguard related
try {
bomk bomk = new bomk();
bomk.a = a;
bomk.d = nextLong;
if (a3 != null) {
bomk.b = a3;
}
bomk.c = m15243b(); //some security token
bomo bomo = new bomo();
bomo.b = Build.VERSION.SDK_INT;
bomo.a = 13292018;
bomk.e = bomo;
byte[] bArr = new byte[bomk.getSerializedSize()];
bomk.writeTo(bmmu.a(bArr, 0, bArr.length));
ByteArrayEntity byteArrayEntity = new ByteArrayEntity(bArr);
byteArrayEntity.setContentType("application/octet-stream");
bomj a4 = bomj.a(m15242a(String.valueOf((String) gog.f18639F.a()).concat("/devicekey"), this.f19985b.mo13641a().f10008a, byteArrayEntity));
try {
hnm hnm = this.f19986c;
hnm.f19991d.h("Storing device key...", new Object[0]);
Lock writeLock = hnm.f19995c.writeLock();
writeLock.lock();
try {
openFileOutput = hnm.f19993a.openFileOutput("device_key", 0);
openFileOutput.write(bmnd.toByteArray(a4));
hnm.f19994b = a4;
openFileOutput.close();
writeLock.unlock();
this.f19987d = true;
return true;
} catch (FileNotFoundException e) {
throw new IOException("File could not be created to store device key.", e);
} catch (Throwable th) {
writeLock.unlock();
throw th;
}
} catch (IOException e2) {
f19984h.e("Error storing key: ", e2, new Object[0]);
this.f19987d = false;
return false;
}
} catch (IOException e3) {
f19984h.e("IOException while requesting key: ", e3, new Object[0]);
}
}
}

Decompiled GMSCore Libraries: https://github.com/Romern/gms_decompiled (unfortunately due to protobuf very unreadable)
Nice tool for analyzing Protobuf: https://protogen.marcgravell.com/decode (also charles is pretty good)

Layout in python:

import betterproto
import base64

@dataclass
class AppCertificate(betterproto.Message):
	appname: str  = betterproto.string_field(1) # e.g. com.tier.app
	appcertbase64: str = betterproto.string_field(3) # base64.b64encode(bytes.fromhex(APP_SIG)).decode("utf-8")
  # APP_SIG: Signature of the Package (getSignature(mActivity.getPackageManager(), packageName);)

@dataclass
class GoogleSpatulaHeader(betterproto.Message):
	cert: "AppCertificate" = betterproto.message_field(1)
	hmac: str = betterproto.string_field(2) #HMACSHA256 of appname+appcertbase64 signed with device_key
	deviceId: int = betterproto.sint64_field(3) #unique ID of the device
	unknown3: int = betterproto.sint64_field(4) #
	unknown4: str = betterproto.string_field(5) #73 chars, begins with 00 5E 6A 80 9C

spathead = GoogleSpatulaHeader()
spathead.cert.appname = tier_app_name
spathead.cert.appcertbase64 = base64.b64encode(bytes.fromhex(android_cert)).decode("utf-8")
spatulaheader = base64.encodebytes(spathead.SerializeToString()).decode("utf-8")

@dataclass
class DeviceKey(betterproto.Message):
	unknown1: int = betterproto.sint64_field(1)
	deviceId: int = betterproto.sint64_field(3)
	unknown2: str = betterproto.string_field(4) # maybe again an HMAC?
	unknown3: str = betterproto.string_field(5) # same layout as spathead.unknown4, maybe public key or something? 
  • Header is generated on the device using the device_key, which is generated using DroidGuard.
  • Spatula Header needed for FirebaseAuth in Android apps using PhoneAuth, apps using GoogleSignInActivity, EmalPassword, AnonymousAuth send the header, but not including does not result in error.
  • Instead of using the Spatula header in PhoneAuth, recaptcha (recaptchaToken) can be used instead ( https://firebase.google.com/docs/auth/web/phone-auth )
import requests
import json
from dataclasses import dataclass
import betterproto
import base64
deviceId = "REDACTED"
spatula_header = 'REDACTED'
tier_app_cert = '27DE32A86E091DCDAA4F1E4209D9FAB96F014520' # Signature of the Package (getSignature(mActivity.getPackageManager(), packageName);)
tier_api_key = 'iPtAHWdOVLEtgkaymXoMHVVg'
tier_app_name = 'com.tier.app'
tier_app_id = "1:511116665713:android:949b292378442e73"
tier_sender_id = "511116665713"
tier_app_key = "AIzaSyDBNn1MiG3v9QQBSbM9VmPe8Jwj5pem5pQ"
tierVersion="3.7.0"
#Spatula Header consisting of "device_key"?
#/data/app/com.google.android.gms-lu9KLM6y-q82m5WeK79rjA==/oat/arm/base.vdex:device_key
#/data/dalvik-cache/arm/system@app@YouTube@YouTube.apk@classes.vdex:device_key
#/system/priv-app/GmsCore/oat/arm/GmsCore.vdex
googleapis_header = {
'Content-Type': 'application/x-protobuf',
'X-Firebase-Locale': "",
'X-Client-Version': 'Android/GmsCore/X19002000/FirebaseCore-Android',
'Accept-Language': 'en-US',
'X-Android-Package': tier_app_name,
'X-Android-Cert': tier_app_cert,
'X-Goog-Spatula': spatula_header,
'User-Agent': 'Mozilla 5.0 (Linux; U; Android 8.1.0; en_US; Moto G (4); Build/OPJ28.111-22); com.google.android.gms/201216018; FastParser/1.1; ApiaryHttpClient/1.0; (gzip) (athene OPJ28.111-22); gzip'
}
googleapis_params = {
"alt": "json",
"key": tier_app_key
}
tier_header = {
'customer-agent': 'Tier Android',
'x-api-key': tier_api_key,
'user-agent': f'Tier/{tierVersion} (Android/8.1.0)'
}
@dataclass
class SimpleMessage(betterproto.Message):
message: str = betterproto.string_field(1)
@dataclass
class VerifyPhoneNumberCode(betterproto.Message): # i guess i could have just used json, look on github of other implementations
token: str = betterproto.string_field(1)
code: str = betterproto.string_field(3)
#https://reverseengineering.stackexchange.com/questions/23216/what-is-the-header-x-goog-spatula
@dataclass
class AppCertificate(betterproto.Message):
appname: str = betterproto.string_field(1)
appcertbase64: str = betterproto.string_field(3)
@dataclass
class GoogleSpatulaHeader(betterproto.Message):
cert: "AppCertificate" = betterproto.message_field(1)
certificatemac: str = betterproto.string_field(2)
deviceId: int = betterproto.sint64_field(3)
unknown3: int = betterproto.sint64_field(4)
unknown4: str = betterproto.string_field(5)
#https://protogen.marcgravell.com/decode
spathead = GoogleSpatulaHeader()
spathead.cert.appname = tier_app_name
spathead.cert.appcertbase64 = base64.b64encode(bytes.fromhex(tier_app_cert)).decode("utf-8")
spathead.certificatemac = "".join(["X" for i in range(32)]) #placeholder, fixed length, HMACSHA of certificate name + hash
spathead.deviceId = #zigzag encoding of deviceId
spathead.unknown3 = #zigzag encoding of 18 byte stuff?
spathead.unknown4 = "".join(["X" for i in range(73)]) #placeholder, can apparently be of variable length
#"-".join(["{:02x}".format(c).upper() for c in spathead.SerializeToString()]) # for comparing
#spatula = base64.encodebytes(spathead.SerializeToString()).decode("utf-8")
###END
## device_key
@dataclass
class DeviceKey(betterproto.Message):
unknown1: int = betterproto.sint64_field(1) # SecureRandom.nextLong()
deviceId: int = betterproto.sint64_field(3)
unknown2: str = betterproto.string_field(4) # maybe again an HMAC?
unknown3: str = betterproto.string_field(5) # same layout as spathead.unknown4, maybe public key or something?
def registerFirebaseApp(): # This is already done in MicroG
header = { 'Authorization': 'AidLogin REDACTED:REDACTED',
'content-type': 'application/x-www-form-urlencoded'}
params = { "X-subtype": tier_sender_id,
"sender": tier_sender_id,
"X-appid":"efxEwFgbR8OWKdzhnCyfqg", # where is this from?
"X-scope": "*",
"X-Goog-Firebase-Installations-Auth": "REDACTED",
"app": tier_app_name,
"device": deviceId}
ret = requests.post("https://android.clients.google.com/c2dm/register3", headers = header, params=params)
return ret
def getFireBaseAuthTokens():
"""Returns:
{'name': 'projects/511116665713/installations/ezBSnpm_NeALYYlf_j6Sa0',
'fid': 'ezBSnpm_NeALYYlf_j6Sa0',
'refreshToken': 'REDACTED',
'authToken': {'token': 'REDACTED',
'expiresIn': '604800s'}}
"""
jsondata = {
"fid":"cooG6FefRlmSMiSZ5jXXSv", #where is this derived? # random: https://github.com/firebase/firebase-android-sdk/blob/f3709ba3b71453ff34cd31d9f01e68af1db40659/firebase-installations/src/main/java/com/google/firebase/installations/RandomFidGenerator.java#L49
"appId":"1:511116665713:android:949b292378442e73", #google_app_id
"authVersion":"FIS_v2",
"sdkVersion":"a:16.0.0"
}
headers = {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-Android-Package': tier_app_name,
'x-firebase-client': 'fire-core/19.3.0 fire-fst/21.4.1 fire-installations/16.0.0 kotlin/1.3.61 fire-iid/20.1.1 fire-android/ fire-analytics/17.2.3 fire-auth/19.2.0',
'x-firebase-client-log-type': '3',
'X-Android-Cert': tier_app_cert,
'User-Agent': 'Dalvik/2.1.0 (Linux; U; Android 8.1.0; Moto G (4) Build/OPJ28.111-22)',
}
params = {
'key': tier_app_key
}
ret = requests.post('https://firebaseinstallations.googleapis.com/v1/projects/consumer-app-release/installations', json=jsondata, headers=headers, params=params)
return ret
def sendVerificationCode(phoneNumber):
"""
Returns
{"sessionInfo":""}
"""
data = SimpleMessage(message=phoneNumber).SerializeToString()
ret = requests.post('https://www.googleapis.com/identitytoolkit/v3/relyingparty/sendVerificationCode',headers=googleapis_header, params=googleapis_params, data=data)
return ret
def verifyPhoneNumber(token, code):
"""
Returns:
{'idToken': string,
'refreshToken': string,
'expiresIn': int as string,
'localId': string,
'isNewUser': bool,
'phoneNumber': string}
"""
data = VerifyPhoneNumberCode(token=token, code=code).SerializeToString()
ret = requests.post('https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPhoneNumber',headers=googleapis_header, params=googleapis_params, data=data)
return ret
def getAccountInfo(idToken):
"""
Returns:
{'kind': 'identitytoolkit#GetAccountInfoResponse',
'users': [{ 'localId': 'REDACTED',
'providerUserInfo': [{ 'providerId': 'phone',
'rawId': 'REDACTED',
'phoneNumber': 'REDACTED'}],
'lastLoginAt': 'REDACTED',
'createdAt': 'REDACTED',
'phoneNumber': 'REDACTED',
'lastRefreshAt': 'REDACTED'}]}
"""
data = SimpleMessage(message=idToken).SerializeToString()
ret = requests.post('https://www.googleapis.com/identitytoolkit/v3/relyingparty/getAccountInfo',headers=googleapis_header, params=googleapis_params, data=data)
return ret
def refreshToken(token):
"""
Returns:
{'access_token': 'REDACTED',
'expires_in': '3600',
'token_type': 'Bearer',
'refresh_token': 'REDACTED',
'user_id': 'REDACTED',
'project_id': '511116665713'
}
"""
jsondata = {"grant_type": "refresh_token",
"refresh_token": token}
jsonheader = googleapis_header.copy()
jsonheader['Content-Type'] = 'application/json'
ret = requests.post('https://securetoken.googleapis.com/v1/token',headers=jsonheader, params=googleapis_params, json=jsondata)
return ret
# Tier APIs
# Registering:
def acceptTermsAndConditions(firebaseidToken):
jsondata = {"termsAndCondition": True}
ret = requests.post('https://consumers.tier-services.io/customer/terms-and-conditions', headers={**tier_header, "x-firebase-auth": firebaseidToken}, json=jsondata)
return ret
def registerAccount(firstName, lastName, email, phoneNumber, firebaseidToken):
jsondata = {"firstName":firstName,
"lastName":lastName,
"email":email,
"phoneNumber":phoneNumber,
"locale":"en-US"}
ret = requests.post('https://consumers.tier-services.io/customer/registration', headers={**tier_header, "x-firebase-auth": firebaseidToken}, json=jsondata)
return ret
# Without Auth:
def checkVersion():
ret = requests.get(f'https://platform.tier-services.io/v1/check-version/tier-consumer/android/{tierVersion}', headers=tier_header)
return ret
def getConfig():
ret = requests.get('https://consumers.tier-services.io/config', headers=tier_header)
return ret
def getZone(lat, lng):
params = {
"lat": lat,
"lng": lng
}
ret = requests.get('https://platform.tier-services.io/zone', headers=tier_header, params=params)
return ret
def getFeatures(lat, lng, countryCode="de"):
data = {"location":{"lat":lat,
"lng":lng},
"traits":{ "country_code":countryCode,
"device_os":"android",
"app_version":tierVersion,
"env":"production"},
"requestedFeatures":["after_ride_picture",
"paypal",
"text_recognition",
"vehicle_preselection",
"my_tier",
"my_tier_two_buttons",
"telesign",
"in_app_vehicle_reporting",
"incentivized_parking",
"paris_parking",
"pricing_v2",
"user_swapping",
"ride_history",
"driver_license",
"custom_sms_verification_service",
"pause_rental",
"in_app_shop",
"filter_emoped"]}
ret = requests.post('https://features.tier-services.io/allocation', headers={**tier_header, "authorization": "Token cAb8lATpDV1fQfYqG0oHkaZOKF90pmxa"}, params=params)
return ret
def getVehicles(lat, lng, radius=508, types=["escooter", "emoped"]):
params = {
"lat": lat,
"lng": lng,
"radius": radius,
"type[]": types
}
ret = requests.get('https://platform.tier-services.io/v2/vehicle', headers=tier_header, params=params)
return ret
def getPricing(vehicleId):
params = {
"vehicleId": vehicleId
}
ret = requests.get('https://platform.tier-services.io/v2/pricing', headers=tier_header, params=params)
return ret
# With Auth:
def getCustomerData(firebaseidToken):
ret = requests.get('https://consumers.tier-services.io/customer', headers={**tier_header, "x-firebase-auth": firebaseidToken})
return ret
def getPaymentMethod(firebaseidToken):
ret = requests.get('https://platform.tier-services.io/v1/payment-method', headers={**tier_header, "x-firebase-auth": "Bearer "+firebaseidToken})
return ret
def getCurrentRental(firebaseidToken):
ret = requests.get('https://platform.tier-services.io/v1/rental/current', headers={**tier_header, "x-firebase-auth": "Bearer "+firebaseidToken})
return ret
def getWallets(firebaseidToken):
ret = requests.get('https://platform.tier-services.io/v1/wallets', headers={**tier_header, "x-firebase-auth": "Bearer "+firebaseidToken})
return ret
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment