Skip to content

Instantly share code, notes, and snippets.

@tombulled
Last active April 12, 2024 03:28
Show Gist options
  • Save tombulled/d313c54a0681fcf0ba6d8092f11411e6 to your computer and use it in GitHub Desktop.
Save tombulled/d313c54a0681fcf0ba6d8092f11411e6 to your computer and use it in GitHub Desktop.
YTApiaryDeviceCrypto implemented in Python 3. Original by @leptos-null (https://gist.github.com/leptos-null/8792b9c50fddc00cf525ed5055a872dc). A working version can be found at the end of the gist (https://gist.github.com/tombulled/d313c54a0681fcf0ba6d8092f11411e6#gistcomment-3069388)
# Go here for the working version: https://gist.github.com/tombulled/d313c54a0681fcf0ba6d8092f11411e6#gistcomment-3069388
import hashlib
from pprint import pprint
import base64
import bytebuffer # https://github.com/alon-sage/python-bytebuffer, pip install bytebuffer
# Other ByteBuffer classes I've found: https://github.com/iGio90/PyByteBuffer, https://github.com/aglyzov/bytebuffer
import secrets
import pyaes # https://github.com/ricmoo/pyaes, pip install pyaes
"""
This code is a Python translation of LMApiaryDeviceCrypto, originally written in Objective-C.
https://gist.github.com/leptos-null/8792b9c50fddc00cf525ed5055a872dc
The LMApiaryDeviceCrypto class was reverse engineered by Leptos from YouTube by Google.
This version does *not* work, however a working version can be found at the end of the gist
I used Teleriks Fiddler as a proxy, enabled https decrypting, installed and trusted their https certificate on my phone,
then connected to the proxy server from my phone to sniff the https packets.
Things I'm unsure of:
1) Is the HTTP Body the request body, or the response body?
2) (See code comments)
"""
PROJECT_KEY = b'WrM95onSB5FfXofSzKWgkNZGiosfmCCAcTH4htvkuj4=' # YouTube Music Base64 Encoded Project Key
API_KEY = 'AIzaSyDK3iBpDP9nHVTk2qL73FLJICfOC3c51Og' # One of YouTube Musics API Keys
HMAC_LENGTH = 4 # LMApiaryDeviceCrypto.h specifies '@param hmacLength Specify 4'?
CC_SHA1_DIGEST_LENGTH = 20
URL = 'https://youtubei.googleapis.com/youtubei/v1/browse?key=AIzaSyDK3iBpDP9nHVTk2qL73FLJICfOC3c51Og' # The test url to sign
HTTP_BODY = [0x0A, 0xB8, 0x05, 0x0A, 0xCA, 0x02, ..., 0x92, 0x01, 0x00, 0xF8, 0x01, 0x03] # Unfortunately this also contained personal information, so has been redacted
HTTP_BODY = bytes(HTTP_BODY) # Not sure if this is the correct http body, but this is the (protobuf?) request body it sent to the api server.
class ByteBuffer2: # Is this better than @alon-sage's bytebuffer? (I found this on github somewhere, but can't find it again)
"""
Bytebuffer of flexible size
"""
def __init__(self, size=None):
self.__size = size
self.__bytebuffer = bytearray()
def put(self, b):
return self.__put(b)
def __put(self, b):
self.__bytebuffer.extend(b)
def put_int(self, i):
"""
Adding an integer to the bytebuffer.
Adds 3 bytes.
:param i: Integer
"""
self.__put(i.to_bytes(3, 'big'))
def put_long(self, l):
"""
Adding a 'Long' to the bytebuffer.
Adds 8 bytes.
(Python doesn't truly deal with longs, so give an integer with the same maxsize as a long.)
:param l: Integer 'Long'
"""
self.__put(l.to_bytes(8, 'big'))
def get_bytebuffer(self):
"""
Return the bytebuffer (type bytearray())
:return: bytearray() bytebuffer
"""
return bytes(self.__bytebuffer)
def get_length(self):
return len(self.__bytebuffer)
class NetCryptoError(Exception): pass
class ApiaryDeviceCrypto(object):
def __init__(self, project_key, sign_length): # project_key (bytearray), sign_length (int)
self.hmac_length = sign_length
internal_hmac_length = 0x10
project_key_length = len(project_key)
if project_key_length >= internal_hmac_length:
project_key = project_key[:internal_hmac_length]
self.hmac_key = project_key[internal_hmac_length:project_key_length - internal_hmac_length]
self.project_key = project_key
def set_device_components(self, id, key): # id (string), key (string)
self.device_key = self.decrypt_encoded_string(key)
self.device_id = id
def sign_connection(self, url, http_body): # url (bytes), http_body (bytes)
signed_url = self.sign_data(url, True, HMAC_LENGTH)
signed_content = self.sign_data(http_body, False, CC_SHA1_DIGEST_LENGTH)
compound_value = f'device_id={self.device_id},data={signed_url},content={signed_content}'
return {'X-Goog-Device-Auth': compound_value}
def sign_data(self, data, pad, hmac_length):
digest = hashlib.sha1()
digest.update(self.device_key)
hashed_data = digest.digest()[:4]
# Pad data here?
append_length = hmac_length # min(hmac_length, ...)?
zero_byte = bytes([0])
new_data = bytebuffer.ByteBuffer.allocate(len(hashed_data) + append_length + 1)
new_data.put(bytearray(zero_byte))
new_data.put(bytearray(hashed_data))
new_data.put(bytearray(hmac.new(self.device_key, data, hashlib.sha1)), 0, append_length)
encoded_data = base64.b64encode(new_data.get_bytes()).rstrip(b'=') # Is this equivelant to Base64 encoding 'without padding'?
return encoded_data.decode('utf-8')
def perform_crypto(self, data, output_len, iv, operation):
key = self.project_key[:0x10]
aes = pyaes.AESModeOfOperationCTR(key)
if operation == 'encrypt': return aes.encrypt(data)[:output_len] # Does the iv need to be used here?
elif operation == 'decrypt': return aes.decrypt(data)[:output_len] # Does the iv need to be used here?
def padded_data(self, data):
pad_mod = 0x10
data_length = len(data)
length_mod = data_length % pad_mod
if length_mod != 0:
pad_data = data + b'\x00' * (pad_mod - length_mod) # Is this the correct way of padding the data?
return pad_data
else:
return data
def project_key_signature(self):
magic = 0x10000000000000001
data = ByteBuffer2() # Note: using a different bytebuffer here
data.put_long(magic)
data.put(self.project_key)
data.put_long(magic)
data.put(self.hmac_key)
digest = hashlib.sha1()
digest.update(data.get_bytebuffer())
return digest.digest()[:4]
def decrypt_encoded_string(self, encoded):
decoded = base64.b64decode(encoded)
first_byte = decoded[0]
if int(first_byte) == 0:
if len(decoded) > 0xc:
low_pad = self.padded_data(decoded[5:8])
some_val = len(decoded) - self.hmac_length - 0xd
high_pad = self.padded_data(decoded[0xd:some_val])
if some_val >= 0:
if self.verify_signed_data(decoded):
high_pad = self.padded_data(decoded[0xd:some_val])
return self.perform_crypto(high_pad, some_val, low_pad, 'decrypt')
else:
raise NetCryptoError("Could not verify encrypted data")
else:
raise NetCryptoError("Could not determine cipher")
else:
raise NetCryptoError("Could not determine initializion vector")
else:
raise NetCryptoError("Could not determine key sign")
def encrypt_and_encode(self, data):
zero_byte = bytes([0])
project_sig = self.project_key_signature()
iv_data = secrets.token_bytes(8) # Is this a correct iv, is it needed?
crypto = self.perform_crypto(self.padded_data(data), len(data), self.padded_data(iv_data), 'encrypt')
ret_pre = len(project_sig) + len(iv_data) + len(crypto)
bytebuffer.ByteBuffer.allocate(ret_pre + self.hmac_length)
mut_data.put(bytearray(zero_byte))
mut_data.put(bytearray(project_sig))
mut_data.put(bytearray(iv_data))
mut_data.put(bytearray(crypto))
magic_byte = bytes([83])
more_data = bytebuffer.ByteBuffer.allocate(ret_pre + 9)
more_data.put(bytearray(magic_byte))
more_data.set_position(9)
more_data.put(mut_data._array, 0, ret_pre)
mut_data.put(hmac.new(self.hmac_key, more_data.get_bytes(), hashlib.sha1), 0, self.hmac_length)
encoded_data = base64.b64encode(mut_data.get_bytes())
return encoded_data.decode('utf-8')
def verify_signed_data(self, data):
project_hash = data[1:4]
if project_hash == self.project_key_signature:
length_diff = len(data) - self.hmac_length
if length_diff >= 0:
high_data = data[length_diff:self.hmac_length]
low_data = data[0:length_diff]
mut_data = bytebuffer.ByteBuffer.allocate(length_diff + 9)
magic_byte = bytes([83])
mut_data.put(bytearray(magic_byte))
mut_data.set_position(9)
mut_data.put(low_data)
check_data = hmac.new(self.hmac_key, mut_data.get_bytes(), hashlib.sha1)
return high_data == check_data
return False
def pad_b64(string):
remainder = len(string) % 4
if remainder:
return string + '=' * (4 - remainder)
return string
# Found by using 'requests' as follows: (pip install requests)
# requests.post('https://youtubei.googleapis.com/deviceregistration/v1/devices?key=AIzaSyDK3iBpDP9nHVTk2qL73FLJICfOC3c51Og&rawDeviceId=RAW_DEVICE_ID').json()
device = \
{
'id': '<<Not sure if private, so have redacted>>',
'key': '<<Not sure if private, so have redacted>>',
}
adc = ApiaryDeviceCrypto(base64.b64decode(PROJECT_KEY), HMAC_LENGTH)
adc.set_device_components(device['id'], pad_b64(device['key'])) # Haven't got past this yet, keep getting: NetCryptoError: Could not verify encrypted data
# signed_url = adc.sign_data(URL.encode(), True, HMAC_LENGTH)
# adc.sign_connection(URL.encode(), HTTP_BODY)
@SuhatAkbulak
Copy link

hello sir How do we create an "HTTP BODY"?.I ran this library but "File" yt-apiary-device-crypto.py ", line 95 compound_value = f'device_id = {self.device_id}, data = {signed_url}, content = {signed_content} '" I get such an error.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment