Skip to content

Instantly share code, notes, and snippets.

@leptos-null
Last active April 12, 2024 03:28
Show Gist options
  • Save leptos-null/8792b9c50fddc00cf525ed5055a872dc to your computer and use it in GitHub Desktop.
Save leptos-null/8792b9c50fddc00cf525ed5055a872dc to your computer and use it in GitHub Desktop.
Fully implemented mirror of YouTube's YTApiaryDeviceCrypto class

LMApiaryDeviceCrypto

I was interested in what would go into writing my own lightweight YouTube Music client. This was my step one.

Steps to Step 1

With any client, there's a server. To find out how I could write a client, I needed to find out how Google's client communicated with the server. After inspecting a the HTTP traffic, I came to the conclusion there were four things I would need:

  1. API key

  2. Authorization HTTP header field

  3. X-Goog-Device-Auth HTTP header field

  4. X-Goog-Visitor-Id HTTP header field

The API key was easy to get. YouTube includes it in the standard GoogleService-Info.plist (AIzaSyC4SSoMBxVCNqJJEIuxYZa5WVFqZUurXjc), and YouTube Music included a few in the binary. A quick strings and grep turned up five keys:

AIzaSyBmltRCNALB9rnWNIiy5FUd-LpDVvYYbGE
AIzaSyAdz24CjJsPc74_a3hV9nxIJSburJjtJe8
AIzaSyDK3iBpDP9nHVTk2qL73FLJICfOC3c51Og
AIzaSyA5iahgfY6tRS9bOBoWAcosfzDu69-juHo
AIzaSyDDAbQV7WXHydhJm-qArV2aCSKs1reexhk

I found this interesting. As far as I could tell, two of these were used in the app. Some of the ones I tested seemed to be registered in a database somewhere, but not fully setup: YouTube Internal API (InnerTube) has not been used in project 75882956776 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/youtubei.googleapis.com/overview?project=75882956776 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.

With this out of the way, I moved on to the Authorization HTTP header field. It seemed like this was hardcoded as well, however I later found out that was incorrect. Since I didn't know that, I moved onto the device X-Goog-Device-Auth HTTP header field.

Reverse Engineering

YTApiaryDeviceCrypto is responsible for "signing" NSMutableURLRequests in YouTube clients on iOS. To be able to make my own requests, I would need the functionality of this class too. I decided the best way to do that was reimplement the class. Using Hopper, cycript, and MobileSubstrate, I was able to do this.

I ran many tests, and after I was confident my implementation was the same as Google's, I wanted to share, in the hopes that others find it useful. I documented the header to my best abilities with helpful, relevant information.

//
// LMApiaryDeviceCrypto.h
//
// Created by Leptos on 11/18/18.
// Copyright © 2018 Leptos. All rights reserved.
//
#import <Foundation/Foundation.h>
#define kYouTubeBase64EncodedProjectKey @"vOU14u6GkupSL2pLKI/B7L3pBZJpI8W92RoKHJOu3PY="
#define kYouTubeMusicBase64EncodedProjectKey @"WrM95onSB5FfXofSzKWgkNZGiosfmCCAcTH4htvkuj4="
/// Fully implemented mirror of @c YTApiaryDeviceCrypto.
/// Some implementations have been slightly edited,
/// however anything created with either class will work with the other,
/// including Archives, however this is SecureCoding, and YT is not.
/// An instance of this class should be created once, and stored to disk using @c NSKeyedArchiver.
/// This class is able to sign @c NSMutableURLRequest objects for interaction with private YouTube REST API.
@interface LMApiaryDeviceCrypto : NSObject <NSSecureCoding>
/// For convenience, the Base64 encoded values of the project keys for YouTube and YouTube Music are included above.
/// @param hmacLength Specify 4
- (instancetype)initWithProjectKey:(NSData *)projectKey HMACLength:(NSUInteger)hmacLength;
/// Before signing a URL request, the deviceID and deviceKey must be set.
/// A valid deviceID and deviceKey can be obtained by making an HTTP POST to
/// https://youtubei.googleapis.com/deviceregistration/v1/devices?key=API_KEY&rawDeviceId=RAW_DEVICE_ID
/// where API_KEY is a valid API key, and RAW_DEVICE_ID is any nonnull string (may support only UUIDs in the future)
- (BOOL)setDeviceID:(NSString *)deviceID deviceKey:(NSString *)deviceKey error:(NSError **)error;
/// Signing a URL request uses all the compotents of this class- ensure they are all valid before calling.
/// The signature is placed in the @c X-Goog-Device-Auth HTTP header field.
/// Mutating the URL or HTTPBody of the @c request after signing it is invalid- other header fields are safe
- (BOOL)signURLRequest:(NSMutableURLRequest *)request error:(NSError **)error;
/// Decrypt an encoded string that was encrypted with the same mechanism, and project key.
- (NSData *)decryptEncodedString:(NSString *)encodedString error:(NSError **)error;
/// Ecrypt and encode any given data. The result is unlikey to be the same for any given data,
/// however the input to this method will always be equal to the output of @c decryptEncodedString:error:
/// when the output of this method is passed as the input to the decrypt method.
/// @code
/// LMApiaryDeviceCrypto *deviceCrypto = ...;
/// NSData *startData = ...;
/// NSString *encryptAlpha = [deviceCrypto encryptAndEncodeData:startData error:NULL];
/// NSString *encryptBeta = [deviceCrypto encryptAndEncodeData:startData error:NULL];
/// [encryptAlpha isEqualToString:encryptBeta]; // Chance of being YES is 1 << 64
/// NSData *endDataAlpha = [deviceCrypto decryptEncodedString:encryptAlpha error:NULL];
/// NSData *endDataBeta = [deviceCrypto decryptEncodedString:encryptBeta error:NULL];
/// [endDataAlpha isEqualToData:endDataBeta]; // Should always be YES
/// [endDataAlpha isEqualToData:startData]; // Should always be YES
/// @endcode
- (NSString *)encryptAndEncodeData:(NSData *)data error:(NSError **)error;
@end
//
// LMApiaryDeviceCrypto.m
//
// Created by Leptos on 11/18/18.
// Copyright © 2018 Leptos. All rights reserved.
//
#import <CommonCrypto/CommonCrypto.h>
#import "LMApiaryDeviceCrypto.h"
#import "../GoogleOSS/GTMStringEncoding.h" /* https://github.com/google/google-toolbox-for-mac/blob/master/Foundation/GTMStringEncoding.h */
#define kYTApiaryDeviceCryptoDeviceIdKey @"kYTApiaryDeviceCryptoDeviceIdKey"
#define kYTApiaryDeviceCryptoDeviceKeyKey @"kYTApiaryDeviceCryptoDeviceKeyKey"
#define kYTDeviceCryptoProjectKeyKey @"kYTDeviceCryptoProjectKeyKey"
#define kYTDeviceCryptoHMACKeyKey @"kYTDeviceCryptoHMACKeyKey"
#define kYTDeviceCryptoHMACLengthKey @"kYTDeviceCryptoHMACLengthKey"
@interface NSError (LMNetCryptoError)
+ (instancetype)netCryptoErrorWithMessage:(NSString *)message;
@end
@implementation LMApiaryDeviceCrypto {
NSString *_deviceID;
NSData *_deviceKey;
NSData *_projectKey;
NSData *_hmacKey;
NSUInteger _hmacLength;
}
- (instancetype)init {
/* original implementation calls [self class] first? */
return nil;
}
- (instancetype)initWithProjectKey:(NSData *)projectKey HMACLength:(NSUInteger)hmacLength {
if (self = [super init]) {
_hmacLength = hmacLength;
NSUInteger internalHmacLength = 0x10;
NSUInteger projectKeyLength = projectKey.length;
if (projectKeyLength >= internalHmacLength) {
_projectKey = [projectKey subdataWithRange:NSMakeRange(0, internalHmacLength)];
_hmacKey = [projectKey subdataWithRange:NSMakeRange(internalHmacLength, projectKeyLength-internalHmacLength)];
}
}
return self;
}
- (BOOL)setDeviceID:(NSString *)deviceID deviceKey:(NSString *)deviceKey error:(NSError **)error {
NSError *derefError = nil;
_deviceKey = [self decryptEncodedString:deviceKey error:&derefError];
if (derefError) {
if (error) {
*error = derefError;
}
return NO;
} else {
_deviceID = [deviceID copy];
return YES;
}
}
- (BOOL)signURLRequest:(NSMutableURLRequest *)request error:(NSError **)error {
NSData *urlData = [request.URL.absoluteString dataUsingEncoding:NSUTF8StringEncoding];
NSString *signedURL = [self signData:urlData padData:YES HMACLength:4];
NSString *signedContent = [self signData:request.HTTPBody padData:NO HMACLength:CC_SHA1_DIGEST_LENGTH];
NSString *compoundValue = [NSString stringWithFormat:@"device_id=%@,data=%@,content=%@", _deviceID, signedURL, signedContent];
[request setValue:compoundValue forHTTPHeaderField:@"X-Goog-Device-Auth"];
return YES;
}
- (NSString *)signData:(NSData *)data padData:(BOOL)shouldPad HMACLength:(NSUInteger)hmacLength {
uint8_t sha1Digest[CC_SHA1_DIGEST_LENGTH];
CC_SHA1(_deviceKey.bytes, (CC_LONG)_deviceKey.length, sha1Digest);
NSData *hashedData = [NSData dataWithBytes:sha1Digest length:4];
if (shouldPad) {
NSUInteger padDataCapacity = data.length + 1;
NSMutableData *padData = [NSMutableData dataWithCapacity:padDataCapacity];
[padData appendData:data];
padData.length = padDataCapacity;
data = [padData copy];
}
CCHmac(kCCHmacAlgSHA1, _deviceKey.bytes, (size_t)_deviceKey.length, data.bytes, data.length, sha1Digest);
NSMutableData *newData = [NSMutableData data];
uint8_t zeroByte = 0;
[newData appendBytes:&zeroByte length:sizeof(zeroByte)];
[newData appendData:hashedData];
size_t appendLength = sizeof(sha1Digest);
if (hmacLength < appendLength) {
appendLength = hmacLength;
}
[newData appendBytes:sha1Digest length:appendLength];
GTMStringEncoding *stringEncoder = [GTMStringEncoding rfc4648Base64StringEncoding];
stringEncoder.doPad = NO;
return [stringEncoder encode:newData error:NULL];
}
- (NSData *)performCrypto:(NSData *)data outputLength:(NSUInteger)length IV:(NSData *)iv operation:(CCOperation)op {
CCCryptorRef cryptor;
CCCryptorStatus status = CCCryptorCreateWithMode(op, kCCModeCTR, kCCAlgorithmAES, ccNoPadding,
iv.bytes, _projectKey.bytes, 0x10, NULL, 0, 0, kCCModeOptionCTR_BE, &cryptor);
if (status == kCCSuccess) {
size_t cryptorLen = CCCryptorGetOutputLength(cryptor, data.length, true);
NSMutableData *ret = [NSMutableData dataWithLength:cryptorLen];
size_t dataMoved;
status = CCCryptorUpdate(cryptor, data.bytes, data.length, ret.mutableBytes, cryptorLen, &dataMoved);
if (status == kCCSuccess) {
CCCryptorRelease(cryptor);
ret.length = length;
return [ret copy];
}
}
return nil;
}
- (NSData *)paddedData:(NSData *)data {
NSUInteger dataLength = data.length;
NSUInteger lengthMod = dataLength & 0xf;
if (lengthMod) {
NSMutableData *padData = [NSMutableData dataWithLength:dataLength + 0x10 - lengthMod];
[padData replaceBytesInRange:NSMakeRange(0, dataLength) withBytes:data.bytes];
return [padData copy]; /* copy call not in original */
} else {
return data;
}
}
- (NSData *)projectKeySignature {
NSMutableData *data = [NSMutableData dataWithCapacity:_hmacKey.length + 0x20];
uint64_t magic = 0x1000000000000000;
[data appendBytes:&magic length:sizeof(magic)];
[data appendData:_projectKey];
[data appendBytes:&magic length:sizeof(magic)];
[data appendData:_hmacKey];
uint8_t sha1Digest[CC_SHA1_DIGEST_LENGTH];
CC_SHA1(data.bytes, (CC_LONG)data.length, sha1Digest);
return [NSData dataWithBytes:sha1Digest length:4];
}
- (NSData *)decryptEncodedString:(NSString *)encodedString error:(NSError **)error {
GTMStringEncoding *strEnc = [GTMStringEncoding rfc4648Base64StringEncoding];
NSData *decoded = [strEnc decode:encodedString error:error];
uint8_t firstByte;
[decoded getBytes:&firstByte length:sizeof(firstByte)];
if (firstByte == 0) {
if (decoded.length > 0xc) {
NSData *lowPad = [self paddedData:[decoded subdataWithRange:NSMakeRange(5, 8)]];
NSInteger someVal = decoded.length - _hmacLength - 0xd;
if (someVal >= 0) {
if ([self verifySignedData:decoded]) {
NSData *highPad = [self paddedData:[decoded subdataWithRange:NSMakeRange(0xd, someVal)]];
return [self performCrypto:highPad outputLength:someVal IV:lowPad operation:kCCDecrypt];
} else if (error) {
*error = [NSError netCryptoErrorWithMessage:@"Could not verify encrypted data"];
}
} else if (error) {
*error = [NSError netCryptoErrorWithMessage:@"Could not determine cipher"];
}
} else if (error) {
*error = [NSError netCryptoErrorWithMessage:@"Could not determine initializion vector"];
}
} else if (error) {
*error = [NSError netCryptoErrorWithMessage:@"Could not determine key sign"];
}
return nil;
}
- (NSString *)encryptAndEncodeData:(NSData *)data error:(NSError **)error {
NSMutableData *mutData = [NSMutableData data];
int8_t zeroByte = 0;
[mutData appendBytes:&zeroByte length:sizeof(zeroByte)];
[mutData appendData:[self projectKeySignature]];
uint8_t buff[8]; /* however you want this to be 8 bytes; could use a single uint64_t */
arc4random_buf(buff, sizeof(buff));
NSData *ivData = [NSData dataWithBytes:buff length:sizeof(buff)];
[mutData appendData:ivData];
NSData *crypto = [self performCrypto:[self paddedData:data] outputLength:data.length IV:[self paddedData:ivData] operation:kCCEncrypt];
if (crypto) {
[mutData appendData:crypto];
NSMutableData *moreData = [NSMutableData dataWithLength:mutData.length + 9];
uint8_t magicByte = 83;
[moreData replaceBytesInRange:NSMakeRange(0, 1) withBytes:&magicByte];
[moreData replaceBytesInRange:NSMakeRange(9, mutData.length) withBytes:mutData.bytes];
uint8_t sha1Digest[CC_SHA1_DIGEST_LENGTH];
CCHmac(kCCHmacAlgSHA1, _hmacKey.bytes, (size_t)_hmacKey.length, moreData.bytes, moreData.length, sha1Digest);
[mutData appendBytes:sha1Digest length:_hmacLength];
GTMStringEncoding *strEncode = [GTMStringEncoding rfc4648Base64StringEncoding];
return [strEncode encode:mutData error:error];
} else if (error) {
*error = [NSError netCryptoErrorWithMessage:@"Generic crypto error."];
}
return nil;
}
- (BOOL)verifySignedData:(NSData *)data {
NSData *projectHash = [data subdataWithRange:NSMakeRange(1, 4)];
if ([projectHash isEqualToData:[self projectKeySignature]]) {
NSInteger lengthDiff = data.length - _hmacLength;
if (lengthDiff >= 0) {
NSData *highData = [data subdataWithRange:NSMakeRange(lengthDiff, _hmacLength)];
NSData *lowData = [data subdataWithRange:NSMakeRange(0, lengthDiff)];
NSMutableData *mutData = [NSMutableData dataWithLength:lengthDiff + 9];
uint8_t magicByte = 83;
[mutData replaceBytesInRange:NSMakeRange(0, 1) withBytes:&magicByte];
[mutData replaceBytesInRange:NSMakeRange(9, lengthDiff) withBytes:lowData.bytes];
uint8_t hmacBytes[CC_SHA1_DIGEST_LENGTH];
CCHmac(kCCHmacAlgSHA1, _hmacKey.bytes, _hmacKey.length, mutData.bytes, mutData.length, hmacBytes);
NSData *checkData = [NSData dataWithBytes:hmacBytes length:_hmacLength];
return [highData isEqualToData:checkData];
}
}
return NO;
}
// MARK: - Coding
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
if (self = [super init]) {
_deviceID = [aDecoder decodeObjectForKey:kYTApiaryDeviceCryptoDeviceIdKey];
_deviceKey = [aDecoder decodeObjectForKey:kYTApiaryDeviceCryptoDeviceKeyKey];
_projectKey = [aDecoder decodeObjectForKey:kYTDeviceCryptoProjectKeyKey];
_hmacKey = [aDecoder decodeObjectForKey:kYTDeviceCryptoHMACKeyKey];
/* Original uses decodeIntForKey, which is not optimal here */
_hmacLength = [aDecoder decodeIntegerForKey:kYTDeviceCryptoHMACLengthKey];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:_deviceID forKey:kYTApiaryDeviceCryptoDeviceIdKey];
[aCoder encodeObject:_deviceKey forKey:kYTApiaryDeviceCryptoDeviceKeyKey];
[aCoder encodeObject:_projectKey forKey:kYTDeviceCryptoProjectKeyKey];
[aCoder encodeObject:_hmacKey forKey:kYTDeviceCryptoHMACKeyKey];
[aCoder encodeInteger:_hmacLength forKey:kYTDeviceCryptoHMACLengthKey];
}
+ (BOOL)supportsSecureCoding {
return YES;
}
@end
// MARK: -
@implementation NSError (LMNetCryptoError)
+ (instancetype)netCryptoErrorWithMessage:(NSString *)message {
return [NSError errorWithDomain:@"com.google.ios.youtube.Net.ErrorDomain" code:0 userInfo:@{
@"message" : message
}];
}
@end
@leptos-null
Copy link
Author

The main problem I imagine you'll run into is finding an equivalent for NSMutableURLRequest in Java. I'm not very familiar with Java, but I believe java.net.HttpURLConnection is the closest, and it doesn't provide a way to read the HTTP body, that I know of.
LeptosMusic was supposed to be the example project for using this class, but the method I'm using for getting the protobuf classes isn't great, and isn't public.

@leptos-null
Copy link
Author

leptos-null commented Oct 12, 2019

Below is my Java translation. It was written to be as close to possible to the code above.

/*
 * This code is a Java 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.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * 
 */

package youtube;

import java.util.Arrays;
import java.util.Base64;

import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.net.HttpURLConnection;

import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;

import javax.security.auth.DestroyFailedException;

import java.security.MessageDigest;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;

public class ApiaryDeviceCrypto {

    private String deviceID;
    private byte[] deviceKey;
    private byte[] projectKey;
    private byte[] hmacKey;
    private int hmacLength;

    public ApiaryDeviceCrypto(byte[] projKey, int signLength) {
        this.hmacLength = signLength;

        final int internalHmacLength = 0x10;
        int projectKeyLength = projKey.length;
        if (projectKeyLength >= internalHmacLength) {
            projectKey = Arrays.copyOfRange(projKey, 0, internalHmacLength);
            this.hmacKey = Arrays.copyOfRange(projKey, internalHmacLength, projectKeyLength - internalHmacLength);
        }
    }

    public boolean setDeviceComponents(String id, String key) throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException {
        this.deviceKey = this.decryptEncodedString(key);
        this.deviceID = id;
        return true;
    }

    public boolean signConnection(HttpURLConnection connection) throws InvalidKeyException, NoSuchAlgorithmException, DestroyFailedException {
        byte[] urlData = connection.getURL().toString().getBytes(StandardCharsets.UTF_8);
        String signedURL = this.signData(urlData, true, 4);
        byte[] httpBody = null; // TODO: HTTP body of connection
        String signedContent = this.signData(httpBody, false, 20); // CC_SHA1_DIGEST_LENGTH

        String compoundValue = "device_id=" + this.deviceID + ",data=" + signedURL + "content=" + signedContent;
        connection.setRequestProperty("X-Goog-Device-Auth", compoundValue);
        return true;
    }

    private String signData(byte[] data, boolean shouldPad, int hmacLen) throws NoSuchAlgorithmException, InvalidKeyException, DestroyFailedException {
        MessageDigest digest = MessageDigest.getInstance("SHA-1");
        digest.update(this.deviceKey);
        byte[] hashedData = Arrays.copyOf(digest.digest(), 4);

        if (shouldPad) {
            byte[] padData = new byte[data.length + 1];
            System.arraycopy(data, 0, padData, 0, data.length);
            data = padData;
        }

        Mac authCode = Mac.getInstance("HmacSHA1");
        SecretKeySpec signingKey = new SecretKeySpec(this.deviceKey, authCode.getAlgorithm());
        authCode.init(signingKey);

        int appendLength = Math.min(hmacLen, authCode.getMacLength());
        byte zeroByte = 0;

        ByteBuffer newData = ByteBuffer.allocate(1 + hashedData.length + appendLength);
        newData.put(zeroByte);
        newData.put(hashedData);
        newData.put(authCode.doFinal(data), 0, appendLength);
        signingKey.destroy();

        Base64.Encoder stringEncoder = Base64.getEncoder().withoutPadding();
        ByteBuffer encodedData = stringEncoder.encode(newData);
        byte[] encodedBytes;
        if (encodedData.hasArray()) {
            encodedBytes = encodedData.array();
        } else {
            encodedBytes = new byte[encodedData.position()];
            encodedData.get(encodedBytes);
        }
        return new String(encodedBytes, StandardCharsets.UTF_8);
    }

    private byte[] performCrypto(byte[] data, int outputLen, byte[] iv, int operation) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException {
        Cipher cryptor = Cipher.getInstance("AES/CTR/NoPadding");
        SecretKeySpec key = new SecretKeySpec(Arrays.copyOf(this.projectKey, 0x10), cryptor.getAlgorithm()); // not sure if this is the correct algorithm to pass
        cryptor.init(operation, key);
        return Arrays.copyOf(cryptor.update(data), outputLen); 
    }

    private byte[] paddedData(byte[] data) {
        final int padMod = 0x10;
        int dataLength = data.length;
        int lengthMod = dataLength % padMod;
        if (lengthMod != 0) {
            byte[] padData = new byte[dataLength + padMod - lengthMod];
            System.arraycopy(data, 0, padData, 0, dataLength);
            return padData;
        } else {
            return data;
        }
    }

    private byte[] projectKeySignature() throws NoSuchAlgorithmException {
        ByteBuffer data = ByteBuffer.allocate(this.hmacKey.length + 0x20);
        final long magic = 0x1000000000000000l;
        data.putLong(magic);
        data.put(this.projectKey);
        data.putLong(magic);
        data.put(this.hmacKey);

        MessageDigest digest = MessageDigest.getInstance("SHA-1");
        digest.update(data);
        return Arrays.copyOf(digest.digest(), 4);
    }

    public byte[] decryptEncodedString(String encoded) throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException {
        Base64.Decoder stringDecoder = Base64.getDecoder();
        byte[] decoded = stringDecoder.decode(encoded);
        byte firstByte = decoded[0];
        if (firstByte == 0) {
            if (decoded.length > 0xc) {
                byte[] lowPad = this.paddedData(Arrays.copyOfRange(decoded, 5, 8));
                int someVal = decoded.length - this.hmacLength - 0xd;
                if (someVal >= 0) {
                    if (this.verifySignedData(decoded)) {
                        byte[] highPad = this.paddedData(Arrays.copyOfRange(decoded, 0xd, someVal));
                        return this.performCrypto(highPad, someVal, lowPad, Cipher.DECRYPT_MODE);
                    } else {
                        throw new NetCryptoError("Could not verify encrypted data");
                    }
                } else {
                    throw new NetCryptoError("Could not determine cipher");
                }
            } else {
                throw new NetCryptoError("Could not determine initializion vector");
            }
        } else {
            throw new NetCryptoError("Could not determine key sign");
        }
    }

    public String encryptAndEncode(byte[] data) throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException {
        final byte zeroByte = 0;
        byte[] projectSig = this.projectKeySignature();

        SecureRandom randomGen = new SecureRandom();
        byte[] ivData = new byte[8];
        randomGen.nextBytes(ivData);

        byte[] crypto = this.performCrypto(this.paddedData(data), data.length, this.paddedData(ivData), Cipher.ENCRYPT_MODE);

        int retPre = 1 + projectSig.length + ivData.length + crypto.length;
        ByteBuffer mutData = ByteBuffer.allocate(retPre + this.hmacLength);
        mutData.put(zeroByte);
        mutData.put(projectSig);
        mutData.put(ivData);
        mutData.put(crypto);

        ByteBuffer moreData = ByteBuffer.allocate(retPre + 9);
        byte magicByte = 83;
        moreData.put(magicByte);
        moreData.position(9);
        moreData.put(mutData.array(), 0, retPre);

        Mac hmac = Mac.getInstance("HmacSHA1");
        SecretKeySpec signingKey = new SecretKeySpec(this.hmacKey, hmac.getAlgorithm());
        hmac.init(signingKey);
        assert moreData.hasArray();
        mutData.put(hmac.doFinal(moreData.array()), 0, this.hmacLength);

        Base64.Encoder stringEncoder = Base64.getEncoder().withoutPadding();
        ByteBuffer encodedData = stringEncoder.encode(mutData);
        byte[] encodedBytes;
        if (encodedData.hasArray()) {
            encodedBytes = encodedData.array();
        } else {
            encodedBytes = new byte[encodedData.position()];
            encodedData.get(encodedBytes);
        }
        return new String(encodedBytes, StandardCharsets.UTF_8);        
    }

    private boolean verifySignedData(byte[] data) throws NoSuchAlgorithmException, InvalidKeyException {
        byte[] projectHash = Arrays.copyOfRange(data, 1, 4);
        if (projectHash.equals(this.projectKeySignature())) {
            int lengthDiff = data.length - hmacLength;
            if (lengthDiff >= 0) {
                byte[] highData = Arrays.copyOfRange(data, lengthDiff, hmacLength);
                byte[] lowData = Arrays.copyOfRange(data, 0, lengthDiff);
                ByteBuffer mutData = ByteBuffer.allocate(lengthDiff + 9);

                byte magicByte = 83;
                mutData.put(magicByte);
                mutData.position(9);
                mutData.put(lowData);

                Mac hmac = Mac.getInstance("HmacSHA1");
                SecretKeySpec signingKey = new SecretKeySpec(this.hmacKey, hmac.getAlgorithm());
                hmac.init(signingKey);
                assert mutData.hasArray();
                byte[] checkData = hmac.doFinal(mutData.array());
                return highData.equals(checkData);
            }
        }
        return false;
    }

    // coding/serialization not implemented 

    public class NetCryptoError extends Error {

        // randomly generated
        private static final long serialVersionUID = 5267767227306374604L;

        public NetCryptoError(String message) {
            super(message);
        }

    }
}

@aminerol
Copy link

@leptos-null thanks for the translation that u made that will give me a headstart to port your implementation of youtube music app from iOS to android, also find a way where you can call the innertube api without going through all of this just change the content-type to application/json and provide the body as json
"context":{ "client":{ "clientName":"ANDROID", "clientVersion":"14.33.56" } }, "browseId":"FEwhat_to_watch",
and you will get response as json.
also for the protobuf u can check this repo where u can find all headers for the youtube music that might help https://github.com/Nosskirneh/ios-app-headers/tree/master/com.google.ios.youtubemusic

@SuhatAkbulak
Copy link

Is it possible to do this with php ?

@leptos-null
Copy link
Author

@aminerol thanks for the JSON tip! I’ll probably use that in the project.
I wrote my own tool that output the headers, and implementations, but I wanted to be able to dump the original .protos (see ProtoDump)

@leptos-null
Copy link
Author

@SuhatAkbulak it should be possible to write it in any language. You need some cryptography functions (SHA1 hash, and AES CTR no pad encryption/description), and easy way to manipulate bytes would be helpful.

@tombulled
Copy link

@leptos-null I'm currently trying to convert the Java version above into Python. I've been using pyaes, hmac and hashlib.sha1 for the crypto, and python-bytebuffer (https://github.com/alon-sage/python-bytebuffer) for working with bytes. I can't seem to get it working, and was wondering if you'd be able to help with a Python translation?

@leptos-null
Copy link
Author

@tombulled I can try to help with any specific issues you have.

@tombulled
Copy link

@leptos-null Thank you for your speedy response, I've created a gist so as not to clutter up yours (https://gist.github.com/tombulled/d313c54a0681fcf0ba6d8092f11411e6). I've had to redact some values to ensure I'm not leaking any information. Please do take a look at it and I'd really appreciate any help you're able to give!

@socialAPIS
Copy link

I reversed the last version in android, and it is quite the same. I made it in PHP, if some needs it, i implemented also fetch player request. Feel free to clone my repo

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