Skip to content

Instantly share code, notes, and snippets.

@teidesu
Created June 22, 2021 13:11
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save teidesu/a0ef09d62abf42b6bbf83bb3608a084f to your computer and use it in GitHub Desktop.
Save teidesu/a0ef09d62abf42b6bbf83bb3608a084f to your computer and use it in GitHub Desktop.
VK token refresh script for audio in JS, based on Vodka2 implementation
/*
* VK token refresh script for audio.
* The only dependency is axios.
* Can be simply embedded in your app:
* const { refreshToken } = require('./vk-audio')
* ...
* refreshToken(oldToken)
*
* Note that you will need to use Kate Mobile's token and user agent, for example:
* KateMobileAndroid/52.1 lite-445 (Android 4.4.2; SDK 19; x86; unknown Android SDK built for x86; en)
*
* Based on Vodka2's PHP implementation (available here https://github.com/vodka2/vk-audio-token),
* but simplified and optimized.
*
* (c) teidesu 2019. This script is licensed under GPLv3
*/
const axios = require('axios')
const zlib = require('zlib')
const util = require('util')
const tls = require('tls')
const qs = require('querystring')
gzip = util.promisify(zlib.gzip)
async function doCheckin() {
let { data } = await axios.post(
'https://android.clients.google.com/checkin',
Buffer.from('EAAaKjEtOTI5YTBkY2EwZWVlNTU1MTMyODAxNzFhODU4NWRhN2RjZDM3MDBmOCLjAQq/AQp'
+ 'FZ2VuZXJpY194ODYvZ29vZ2xlX3Nka194ODYvZ2VuZXJpY194ODY6NC40LjIvS0svMzA3OTE4Mzplbmcvd'
+ 'GVzdC1rZXlzEgZyYW5jaHUaC2dlbmVyaWNfeDg2Kgd1bmtub3duMg5hbmRyb2lkLWdvb2dsZUCFtYYGSgt'
+ 'nZW5lcmljX3g4NlATWhlBbmRyb2lkIFNESyBidWlsdCBmb3IgeDg2Ygd1bmtub3duag5nb29nbGVfc2RrX'
+ '3g4NnAAEAAyBjMxMDI2MDoGMzEwMjYwQgttb2JpbGU6TFRFOkgAMgVlbl9VUzjwtN+muZq4g44BUg8zNTg'
+ 'yNDAwNTExMTExMTBaAGIQQW1lcmljYS9OZXdfWW9ya3ADehw3MVE2Um4yRERabDF6UERWYWFlRUhJdGQrW'
+ 'Wc9oAEAsAEA', 'base64'),
{
headers: {
'Content-Type': 'application/x-protobuffer',
'Expect': '',
'User-Agent': 'Android-GCM/1.5 (generic_x86 KK)'
},
responseType: 'arraybuffer',
validateStatus: null
}
)
return parseProtobufResponse(data)
}
function readVarint(data, start = 0n) {
let i = 0
let num = 0n
while (true) {
if (i >= data.length) throw Error('Unexpected EOF when reading varint')
if (BigInt(data[start + i]) & 0x80n) {
num =
num | (
(
BigInt(data[start + i]) ^ 0x80n
) << (
BigInt(7 * i)
)
)
i++
} else {
num =
num | (
BigInt(data[start + i]) << BigInt(7 * i)
)
break
}
}
return {
num,
pos: start + i + 1
}
}
function writeVarint(num) {
let ret = []
num = num - 0
while (num !== 0) {
let t = num & 0x7f
num = num >> 7
if (num !== 0) {
ret.push(t | 0x80)
} else {
ret.push(t)
}
}
return ret
}
function mtalkRequest(authData) {
let idLen = writeVarint(authData.id.length)
let tokenLen = writeVarint(authData.token.length)
let hexId = Buffer.from('android-' + authData.rawid.toString('hex'))
let hexIdLen = writeVarint(hexId.length)
let msg = []
msg.push(...Buffer.from('CgphbmRyb2lkLTE5Eg9tY3MuYW5kcm9pZC5jb20a', 'base64'))
msg.push(...idLen)
msg.push(...Buffer.from(authData.id))
msg.push(0x22)
msg.push(...idLen)
msg.push(...Buffer.from(authData.id))
msg.push(0x2a)
msg.push(...tokenLen)
msg.push(...Buffer.from(authData.token))
msg.push(0x32)
msg.push(...hexIdLen)
msg.push(...hexId)
msg.push(...Buffer.from('QgsKBm5ld192YxIBMWAAcAGAAQKIAQE=', 'base64'))
let len = msg.length
msg.unshift(...writeVarint(len))
msg.unshift(0x29, 0x02)
return Buffer.from(msg)
}
function doMtalkCheckin(authData) {
return new Promise((resolve, reject) => {
const cl = tls.connect(5228, 'mtalk.google.com', null, () => {
cl.write(mtalkRequest(authData))
let first = true
cl.on('data', (data) => {
if (first) {
first = false
return
}
if (data[0] === 0x03) {
resolve()
} else {
reject(Error('Unexpected ' + data[0] + ' while reading MTalk response'))
}
})
cl.on('error', (err) => {
cl.destroy(err)
reject(err)
})
})
})
}
function read64(buf, offset) {
const first = buf[offset]
const last = buf[offset + 7]
if (first === undefined || last === undefined) {
throw Error('Unexpected EOF while reading UInt64')
}
const lo = first +
buf[++offset] * 256 +
buf[++offset] * 65536 +
buf[++offset] * 16777216
const hi = buf[++offset] +
buf[++offset] * 256 +
buf[++offset] * 65536 +
last * 16777216
return BigInt(lo) + (
BigInt(hi) << 32n
)
}
function readFieldWireType(data, start = 0) {
let { num, pos } = readVarint(data, start)
return {
wireType: Number(num) & 0x7,
fieldNum: Number(num) >> 3,
pos
}
}
/**
* @param {Buffer} data
*/
function parseProtobufResponse(data) {
let pos = 0
let id = null
let rawid = null
let token = null
while (true) {
if (pos >= data.length) throw Error('Unexpected EOF while parsing protobuf')
let wt = readFieldWireType(data, pos)
pos = wt.pos
switch (wt.wireType) {
case 0: {
let d = readVarint(data, pos)
pos = d.pos
break
}
case 1: {
if (wt.fieldNum === 7 /* ID */) {
id = read64(data, pos) + ''
rawid = data.subarray(pos, pos + 8)
} else if (wt.fieldNum === 8 /* token */) {
token = read64(data, pos) + ''
}
if (id && token) return { id, rawid, token }
pos += 8
break
}
case 2: {
let d = readVarint(data, pos)
pos = d.pos + Number(d.num)
break
}
default:
throw Error('Unexpected wire type ' + wt.wireType + ' in field ' + wt.fieldNum)
}
}
}
async function getReceipt(authData, userId) {
const doReq = () => axios.post('https://android.clients.google.com/c2dm/register3', qs.stringify(params), {
headers: {
'User-Agent': 'Android-GCM/1.5 (generic_x86 KK)',
'Authorization': 'AidLogin ' + authData.id + ':' + authData.token,
'app': 'com.perm.kate_new_6',
'gcm_ver': '13283005'
},
validateStatus: null,
responseType: 'text'
})
const params = {
'X-scope': 'GCM',
'X-osv': '23',
'X-subtype': '54740537194',
'X-X-subtype': '54740537194',
'X-subscription': '54740537194',
'X-X-subscription': '54740537194',
'sender': '54740537194',
'app_ver': '445',
'X-app_ver': '445',
'X-kid': '|ID|1|',
'X-X-kid': '|ID|1|',
'X-appid': Math.random().toString(36).substr(2, 13),
'X-gmsv': '13283005',
'X-cliv': 'iid-10084000',
'X-app_ver_name': '52.1 lite',
'app': 'com.perm.kate_new_6',
'device': authData.id,
'cert': '966882ba564c2619d55d0a9afd4327a38c327456',
'info': 'w8LuNo60zr8UUO6eTSP7b7U4vzObdhY',
'gcm_ver': '13283005',
'plat': '0',
'X-messenger2': '1'
}
let d = await doReq()
params['X-scope'] = 'id' + userId
params['X-kid'] = params['X-X-kid'] = '|ID|2|'
let { data } = await doReq()
data = data.split('|ID|2|:')[1]
if (data === 'PHONE_REGISTRATION_ERROR') throw Error('PHONE_REGISTRATION_ERROR')
return data
}
async function refreshToken(oldToken) {
const d = await axios.get(`https://api.vk.com/method/users.get?v=5.101&access_token=${oldToken}`)
if (!d.data.response) throw Error('Expected old token to be valid')
let userId = d.data.response[0].id
const authData = await doCheckin()
await doMtalkCheckin(authData)
const receipt = await getReceipt(authData, userId)
const { data } = await axios.post('https://api.vk.com/method/auth.refreshToken', qs.stringify({
access_token: oldToken,
receipt,
v: '5.101'
}), {
headers: {
'User-Agent': 'KateMobileAndroid/52.1 lite-445 (Android 4.4.2; SDK 19; x86; unknown Android SDK built for x86; en)'
}
})
if (data.error) throw data.error
return data.response.token
}
module.exports = {
refreshToken,
doCheckin,
doMtalkCheckin,
readVarint,
read64,
readFieldWireType,
getReceipt,
mtalkRequest
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment