Last active
April 12, 2024 03:28
-
-
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)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.