work-in-progress headless client for love live! school idol festival all stars
follow along with my notes to see how I reverse engineer it
this project is at a very early stage. right now it's meant to be used by me and other developers to reverse engineer the api further and test it. eventually i want this to be a module you can use to automatically manage and create your sifas accounts
do not run multiple instances of this client if you don't know what you're doing, they will end up fighting eachother and potentially breaking accounts. use the daemon mode, which runs everything in one process. the individual applets are for dev testing
temporarily, you need nodejs to run an additional local service that generates push notification tokens. this will run on port 6969
cd token-generator
npm i
npm start
once that's running, you can build and run the client
you need the kotlin compiler (kotlinc) which on void linux is package
kotlin-bin
my build script will automatically download and set up dependencies
# build and run
./build.sh daemon | tee -a output.log
it's recommended to save output to a log as some responses are very large to read with just terminal scrollback
install nodejs, I don't think the additional tools for native extensions are necessary, not sure
download java for windows x64 https://jdk.java.net/java-se-ri/9 and extract somewhere
the java cacerts file will be broken. replace it with this
. you should copy it to path\to\java\lib\security
replacing the cacerts
file
from the cygwin installer select curl, sqlite, sqlite-devel, zip, git. other packages might be necessary that I missed, if you run into any issue re-run cygwin installer to install more pkgs
run (replace paths as appropriate for your installs)
echo 'export PATH="$PATH:/cygdrive/c/path/to/java-9/bin"' >> .bashrc
echo 'export PATH="$PATH:/cygdrive/c/Program Files/nodejs"' >> .bashrc
curl -s https://get.sdkman.io | bash
sdk install kotlin
last command will prompt you to run some init command. run it
then run
source ~/.bashrc
now you can
git clone https://github.com/Francesco149/todokete ~/todokete
cd ~/todokete/token-generator
npm i
npm start
and in another cygwin window:
cd ~/todokete
./build.sh daemon
- store accounts in a database
- log in existing accounts
- configurable endpoint and other version/region tied things
- multithread support
- store full request logs for each account
- a way to view and filter all logs combined
- proxy support
- link sifid.net account
- automatically download apk and parse strings and hashes
this is very rough at the moment, I just needed a quick hack to visualize
accounts and items from the temporary front-end I'm developing. it
literally just dumps all the accounts and items at once as json when you
request /accounts
and lets you get decrypted textures by pak name and
offset. this requires having an "assets" folder that contains a texture
folder with the decrypted folder as well as the decrypted game databases
(masterdata.db
, asset_a_ja_0.db
and all the dictionary db's)
it goes without saying that this backend is not meant to be secure and should not be exposed on an untrusted network. always keep it on your local network, make sure the port is closed to the outside world
endpoints:
/accounts
returns an array of all the accounts. the items field is a map of id -> amount. example:[ { "id": 123, "lastLogin": 1573140519347, "sifidMail": "nice@me.me", "items": {"0":900,"9015":1}, "archived": 0 } ]
/items
returns a map of all items where the keys are the id's. note that this doesn't return all items in the game, just the ones that the accounts from the last/accounts
call happen to have. example:{ "0": { "name": "stars", "description": "free gacha points", "packName": "i0gvmq", "head": 227544 } }
/texture?packName=xxx&head=123
gets a texture by packName and head. this can be used to get item icons. only works with png textures at the moment/sifid?mail=nice@me.me
gets a sif id by mail from sifid.db , please see the frontend documentation below for more info on how to set up this database. if the mail parameter is not specified, a random account that isn't linked to anything is picked example:{ "mail": "nice@me.me", "password": "sekrit", "secretQuestion": "Are traps gay?", "secretAnswer": "...", "birthMonth": 12, "birthDay": 1, "birthYear": 1992 }
/link?id=123123&mail=nice@me.me&password=passw0rd
links a sif id to an account id. empty response. note that this merely initiates the link process. it will do it in the background, so to make sure the account is actually linked you have to keep polling/sifid
with the mail you tried to link until it fails, which will mean it is linked. or simply keep polling accounts until it reports it as linked/archive?id=123123
sets an account as archived, which means it won't be logged in anymore and the archived flag will be set to true. empty response
I have written a quick n dirty front-end in react that lets you visualize accounts and items, as well as link sif id's. you can start it like so
cd frontend
npm i
npm start
note: manually linking a sifid takes time. don't spam click the button.
click it and wait like 30 seconds and it'll finally refresh. ideally you
want some amount of accounts always pre-linked with --link-accounts
instead. manual linking should be used for special cases
the filter field uses filtrex expressions. you can find more detailed info about the syntax here https://github.com/joewalnes/filtrex#expressions
filter fields:
- urtickets: number of ur tickets (item 9015)
- stars: number of free gacha stars (item 0)
- sifidMail: set this as filter to only show accounts that have a sif id linked
- id: account id
- lastLogin: last login timestamp (unix milliseconds)
hoursAgo(x)
: timestamp for x hours ago (unix milliseconds)
it requires the backend to be running and at the moment must be hosted on the same machine/ip that hosts the backend
this can also pull sif id's automatically from a sifid.db sqlite database if present. it must be in the backend folder and requires at least these fields:
create table if not exists sifid(
mail text primary key,
password char[32] not null,
secret_question text not null,
secret_answer text not null,
birth_month integer not null,
birth_day integer not null,
birth_year integer not null,
)
this will allow the frontend to
- automatically pick a random sif id to link from the database
- automatically copy all necessary linked sif id info for a particular account to the clipboard with a single button press
the body of each request is a json array that contains two elements
- a json object
- a hex string representation of the request hash
[{"my_data": "blahblah"},"123456789abcdef123456789abcdef123456789a"]
request headers are the default okhttp3 headers, plus
content-type: application/json
. make sure this header doesn't have
the charset part or the server will refuse
the hash obtained by running hmac-sha1 on a string that contains the request path (including the query string) and the json object, separated by a space
example:
/some/endpoint?u=123 {"my_data": "blahblah"}
the key for this hmac-sha1 hash is the sessionKey, which is initially
set to startupKey. this startupKey changes for every version of the game,
but you can easily extract it by using Il2CppDumper with unity version
2018.4
on libil2cpp.so
and looking for the endpoint string in the
generated script.py
, the startup key will be nearby, it's a 16-character
string. if you can't find it, you're gonna have to load the binary into
ghidra or ida, and load the strings from script.py
(it's an ida script),
then look at ServerConfig$$.cctor
and all the strings it references
you can even just straight up open the game's global-metadata.dat in a hex editor and search for the strings there
the base path of the endpoint (currently /ep1015
) will also change every
version
reponses will also be a json array that contains
- a timestamp
- a master version hash, we will need this later
- unknown integer that is always zero
- response json object
- hash
example:
[1572439123123,"abcdef012345689a",0,{"some_data":"blah"},"abcdef0123456789abcdef0123456789abcdef01"]
the query string for requests can contain a number of parameters, in this order:
- p: platform (a for android). this is always present
- mv: the master version hash. can be obtained from the first response. omitted for the first request
- id: request id. this is a sequential number that starts at 1 and increments for each successful request
- t: unix timestamp in milliseconds. omitted for the first request
- u: user id. omitted if not logged in. obtained from creating an account or recovering account data associated with a service id
order of parameters is important. the server cannot handle them out of order
there's a number of api's that return your password through an
authorization_key
field, which is base64-encoded. each one of them will
require you to send a "mask" field in your request. this mask field is
an array of 32 random bytes, encrypted with the game's public key and
encoded as base64. the server then xors your password with these bytes
before sending it to you. to get your actual password, you have to xor this
back with the random bytes you sent. this password is used as your
sessionKey. your password appears to be determined on account creation and
doesn't seem to change, but it can be recovered through fetchGameService
calls provided that you linked a service id to your accout
the public key can be obtained by searching for a string that
starts with <RSAKeyValue>
in the Il2CppDumper output as with the
startup key. this is a .net xml key string. most other languages will
want it converted to pem format. just take the pem formatted key from
my code, it's probably never gonna change
apis that return your password:
/login/startup
. this is where you create your account/dataLink/fetchGameServiceDataBeforeLogin
gets your account data from the linked service id/dataLink/fetchGameServiceData
same as above but it's an authenticated request signed with your password/dataLink/fetchSchoolIdolFestivalIdDataAfterLogin
get accounts associated with this sifid. it returns link data like fetchGameServiceData
as soon as you obtain your password, you should change your sessionKey from the startup key to the password and sign all authenticated requests with it
once you call /login/login
with your password, you will get yet another
xored authorization_key
which is temporary for that particular session
the startup key is only used for fetchGameServiceDataBeforeLogin and startup as far as I know
some api calls have a device_token
field. this is a firecloud messaging
push notification token, which can be obtained by calling firebase api
with the project number from
base.apk/assets/google-services-desktop.json
some api calls have a asset_state
field. this is an obfuscated string
of bytes generated by libjackpot-core. you can check my code to see how
it's computed. it's not checked at the moment, but it's best to generate
it correctly. the value depends on the first 3 bytes of your random byte
string used in mask, the md5, sha1, sha256 hashes of the package signature,
as well as the md5, sha1, sha256 hashes of libil2cpp.so and
libjackpot-core.so
-
/login/startup
: creates a new account -
/login/login
: log into existing account - properly generating
asset_state
instead of hardcoding it -
/dataLink/fetchGameServiceDataBeforeLogin
gets existing accounts associated with aservice_id
, also used to get MasterVersion on startup -
/terms/agreement
used for checking the currently accepted ToS version and what the latest version to accept is -
/userProfile/setProfile
sets name, nickname, message and push notification token -
/userProfile/setProfileBirthday
sets birth month and day -
/story/finishUserStoryMain
completes a main story chapter -
/live/start
starts a live, receives note data for the chosen song -
/ruleDescription/saveRuleDescription
not sure, I think it has something to do with either gifts or storyline progress status -
/live/finish
completes a live. sends precise note-by-note scoring. accurate simulation of the game's scoring system is required to submit real scores, however for the tutorial lives it's possible to skip by submitting all the notes with zero values as demonstrated in myskipLive
function. this is equivalent to pausing and clicking ok to skip the live -
/communicationMember/setFavoriteMember
sets favorite member, the character who appears in your main screen -
/bootstrap/fetchBootstrap
fetches all kinds of info based on the list of id's provided. things that can be fetched include login bonuses, expired items, new badges, banners, notices, billing info -
/navi/tapLovePoint
sent when you touch your waifu in the main screen -
/navi/saveUserNaviVoice
not 100% sure, I think it has to do with unlocking menu options -
/trainingTree/fetchTrainingTree
supposed to get training locations or something like that but during the tutorial the response is empty -
/trainingTree/levelUpCard
-
/trainingTree/activateTrainingTreeCell
probably sent when you start training. not sure where the cell id's come from -
/communicationMember/finishUserStorySide
complete a character side story -
/card/updateCardNewFlag
used to refresh cards info -
/liveDeck/saveDeckAll
saves live team/s. not sure what cardWithSuit means yet. maybe related to the outfit they're wearing? -
/liveDeck/saveSuit
not sure, but I think it saves the outfit for a particular character -
/livePartners/fetch
fetches a list of live partners. empty request -
/gacha/fetchGachaMenu
gets the list of scouting pools, empty req -
/gacha/draw
scout from a pool -
/tutorial/phaseEnd
ends the tutorial. empty request -
/dataLink/fetchGameServiceData
gets existing accounts associated with aservice_id
-
/dataLink/linkOnStartUpGameService
associates the currently logged account to a service id so it can be later retrieved with fetchGameServiceData or fetchGameServiceDataBeforeLogin -
/loginBonus/readLoginBonus
marks login bonus splashscreens as read. empty response -
/notice/fetchNoticeDetail
gets contents of a specific note. a list of notes can be obtained fromfetch_bootstrap_notice_response
when calling fetchBootstrap -
/notice/fetchNotice
gets various notices, empty request -
/present/fetch
gets a list of pending presents. empty request -
/present/receive
opens presents, takes a list of present id's from/present/fetch
-
/bootstrap/getClearedPlatformAchievement
gets a list of achievement id's (probably linked to stuff like the android game hub or whatever it's called). empty request -
/schoolIdolFestivalIdReward/fetch
gets a list of sif id rewards. empty request -
/dataLink/fetchDataLinks
checks whether you have a sifid linked as well as whatever gaming platform your phone uses. empty request -
/dataLink/fetchSchoolIdolFestivalIdDataAfterLogin
get accounts associated with this sifid. it returns link data like fetchGameServiceData -
/dataLink/linkSchoolIdolFestivalId
link your sifid. empty request, sent after fetchSchoolIdolFestivalIdDataAfterLogin which is probably how the server knows what sifid to link
why kotlin? it just so happens that the http library used by the game is java and I decided to use it for better accuracy. I'm not a big fan of kotlin, it suffers from the same slow compilation as java, which is incredibly slow compared to something like C
I will not put this on maven - the directory structure and packaging process is too ugly and I don't want to tab 10 levels of subdirectories every time I edit a file. I will leave it as a simple single file. if you want to use this in a project feel free to just straight up embed the source file in it