Adobe Pass support and research #11520

Closed
opened 2026-02-21 06:13:46 -05:00 by deekerman · 4 comments
Owner

Originally created by @Tatsh on GitHub (Aug 23, 2017).

Please follow the guide below

  • You will be asked some questions and requested to provide some information, please read them carefully and answer honestly
  • Put an x into all the boxes [ ] relevant to your issue (like this: [x])
  • Use the Preview tab to see what your issue will actually look like

Make sure you are using the latest version: run youtube-dl --version and ensure your version is 2017.08.23. If it's not, read this FAQ entry and update. Issues with outdated version will be rejected.

  • I've verified and I assure that I'm running youtube-dl 2017.08.23

Before submitting an issue make sure you have:

  • At least skimmed through the README, most notably the FAQ and BUGS sections
  • Searched the bugtracker for similar issues including closed ones

What is the purpose of your issue?

  • Bug report (encountered problems with youtube-dl)
  • Site support request (request for adding support for a new site)
  • Feature request (request for a new functionality)
  • Question
  • Other

Anyone else interested in helping with this please contact me.

I have done a bit of research understanding how watching live TV at https://watch.spectrum.net/livetv works and on iOS as well which uses the Adobe SDK. I have not yet completed this research but these are my discoveries:

  • Authentication does use cookies but not always
  • Authentication on the API uses OAuth v1
  • Streams are encrypted, the metadata is embedded in the m3u8 and it is base64 encoded (PKCS#7 DER encoded inside)
  • Your OAuth consumer key and secret (which never change) are found in on the main page in the JavaScript:

GET https://watch.spectrum.net/livetv with no cookies
find line with window.onload = function () {
find next line with var environments = ...
parse this base64-encoded string. sample data:

[{"name":"prod","label":"production","splunk":{"domain":" https://splunk.ngclogging.cloud.twc.net/"},"analytics":{"endpoint":"https://v-collector.dp.aws.charter.com/api/collector"},"vpns":{"baseUri":"https://vpns-gen.timewarnercable.com"},"oAuth":{"consumerKey":"joeigjioegj","s":"jaoiegjoejg"},"displayRawRentError":false,"default":true}]"

Use this OAuth data to get token

Getting the temporary token:

GET https://services.timewarnercable.com/auth/oauth/request
OAuth data required from previous
Returns URL-encoded string with xoauth data
example: oauth_token=xxxx&oauth_token_secret=xxx&xoauth_token_expiration=1503113510937&oauth_callback_confirmed=true

'Authorise' your device:

POST https://services.timewarnercable.com/auth/oauth/device/authorize
OAuth data required from last step
POST data:
xoauth_device_id: xxx
xoauth_device_type: ONEAPP-OVP
oauth_token: xxx
username: your spectrum account username (could be an email)
password: ...

To get a session ID, etc:
Oauth GET https://services.timewarnercable.com/ipvs/api/smarttv/adobe/session
In the JSON: ticketId, sessionId, expiration (UNIX timestamp)

Then get stream information, JSON which will have the URI to an m3u8 file:

OAuth GET:
https://services.timewarnercable.com/ipvs/api/smarttv/stream/live/v1/172?adID=<adId>&csid=stva_ovp_pc_live&dai-supported=true&drm-supported=true&encoding=hls&sessionId=<your session ID>&vast-supported=true

I have no idea what adID is in the above URL.

In the JSON, you will see the key stream_url which is the full m3u8 URL. This is downloadable without cookies or anything (authorisation is in the query string). It is timed and will eventually expire.

In the M3U8, you can find the following:

EXT-X-FAXS-CM key -> base64 decode -> pkcs7 decode
base64 -d file-with-content > faxs.der
openssl pkcs7 -inform der -in faxs.der -print_certs
POST URL is in the metadata
strings -n 10 faxs.der

The POST URL for the license server found in the metadata requires query parameters:

CrmId=twc&AccountId=twc&ContentId=6_ae&SubContentType=Primetime&OriginalUri=/flashaccess/getServerVersion/v3&Ticket=<ticket ID>&SessionId=<SessionId>

ContentId is the channel ID (in this case A&E).

You must POST a certificate as is (possibly URL encoded which seems strange yes) in the body of the request. What you get back is a 'individualization certificate', as in the decryption key?

There are two such requests. I do not know why. They differ in size so they are different.

Once the key is grabbed, I assume any TS file can be decrypted. The algorithm is not really known, but I seem to see references to AES-128-CBC in the Adobe Player SDK on iOS. There is a method named: -[DRMManager initDecryptionSession:playbackSession:error:complete:].

Python code to demonstrate (set global variables USERNAME and PASSWORD):

from base64 import b64decode
from datetime import datetime
from math import trunc
from random import SystemRandom
import json
import logging
import re
import sys

from requests_oauthlib import OAuth1Session
import requests

USER_AGENT = ('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
              '(KHTML, like Gecko) Chrome/61.0.3163.49 Safari/537.36')

URL_LIVE_TV = 'https://watch.spectrum.net/livetv'

# TODO Get URLs dynamically like the site
URL_AUTO_AUTHORIZATION = 'https://services.timewarnercable.com/auth/oauth/auto/authorize'
URL_DEVICE_AUTHORIZATION = 'https://services.timewarnercable.com/auth/oauth/device/authorize'
URL_STATUS = 'https://services.timewarnercable.com/auth/oauth/token/status'
URL_TEMPORARY_REQUEST = 'https://services.timewarnercable.com/auth/oauth/request'
URL_TOKEN_EXCHANGE = 'https://services.timewarnercable.com/auth/oauth/ssotoken/exchange'
URL_TOKEN = 'https://services.timewarnercable.com/auth/oauth/token'
URL_WAYFARER = 'https://services.timewarnercable.com/auth/oauth/wayfarer'
URL_ADOBE_SESSION = 'https://services.timewarnercable.com/ipvs/api/smarttv/adobe/session'
URL_VPNS_CONNECT = 'https://vpns-gen.timewarnercable.com/vpnspush/v1_5/connect'
URL_VPNS_REGISTRATION = 'https://vpns-gen.timewarnercable.com/vpnsservice/v1_5/registration'


# Device ID original JavaScript:
# "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(
#   /[xy]/g,
#  (e) => {
#    var t = (16 * Math.random()) | 0;
#    if (e === 'x') {
#        n = t;
#    } else {
#        n = 3 & t | 8;
#    }
#    return n.toString(16);
# })
def generate_device_id():
    rand = SystemRandom()
    s = [x for x in 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx']
    for i, c in enumerate(s):
        if c == 'x' or c == 'y':
            t = trunc(16 * rand.random()) | 0
            if c == 'x':
                n = t
            else:
                n = 3 & t | 8
            s[i] = hex(n)[2:]
    return ''.join(s)


if __name__ == '__main__':
    log = logging.getLogger('oauthlib')
    log.addHandler(logging.StreamHandler(sys.stdout))
    log.setLevel(logging.DEBUG)

    sess = requests.Session()
    sess.headers.update({'User-Agent': USER_AGENT})

    # GET https://watch.spectrum.net/livetv with no cookies
    r = sess.get(URL_LIVE_TV)
    r.raise_for_status()

    data = None
    for line in r.content.decode('utf-8').split('\n'):
        if 'var environments' not in line:
            continue
        m = re.match(r'(?:\s+)?var(?:\s+)environments(?:\s+)=(?:\s+)[\'"]([^\'"]+)[\'"]', line)
        if not m:
            raise Exception()
        data = json.loads(b64decode(m.group(1)).decode('utf-8'))
    if not data:
        raise Exception()

    oauth_consumer_key = data[0]['oAuth']['consumerKey']
    oauth_secret = b64decode(b64decode(data[0]['oAuth']['s']))

    oauth = OAuth1Session(oauth_consumer_key,
                          client_secret=oauth_secret)
    oauth.headers.update({
        'User-Agent': USER_AGENT,
        'Referer': URL_LIVE_TV,
        'Origin': 'https://watch.spectrum.net',
        'Accept': 'application/json, text/plain, */*',
    })
    token_data = oauth.fetch_request_token(URL_TEMPORARY_REQUEST)

    device_id = generate_device_id()

    r = oauth.post(URL_DEVICE_AUTHORIZATION, data=dict(
        xoauth_device_id=device_id,
        xoauth_device_type='ONEAPP-OVP',
        oauth_token=token_data['oauth_token'],
        username=USERNAME,
        password=PASSWORD,
    ))
    r.raise_for_status()
    dev_auth = 'https://f?oauth_token={}&{}'.format(token_data['oauth_token'], r.content.decode('utf-8'))
    dev_auth = oauth.parse_authorization_response(dev_auth)

    r = oauth.post(URL_TOKEN)
    r.raise_for_status()
    oauth_token = 'https://f?{}'.format(r.content.decode('utf-8'))
    oauth_token = oauth.parse_authorization_response(oauth_token)

    oauth = OAuth1Session(oauth_consumer_key,
                          client_secret=oauth_secret,
                          resource_owner_key=oauth_token['oauth_token'],
                          resource_owner_secret=oauth_token['oauth_token_secret'],
                          verifier=dev_auth['oauth_verifier'])
    oauth.headers.update({
        'User-Agent': USER_AGENT,
        'Referer': URL_LIVE_TV,
        'Origin': 'https://watch.spectrum.net',
        'Accept': 'application/json',
    })

    # VPNS - what is VPNS? :(
    #r = oauth.post(URL_VPNS_REGISTRATION, json={
        #'Registration': {
            #'Device': {
                #'id': device_id,
            #},
            #'id': device_id,
            #'operation': 'create',
        #}
    #})
    #r.raise_for_status()
    #registration_data = r.json()
    #vpns_client_id = registration_data['Registration']['Client']['id']
    #vpns_session_id = r.headers['X-VPNS-NOTIFY-SESSIONID']

    r = oauth.get(URL_ADOBE_SESSION)
    r.raise_for_status()
    session = r.json()
    ticketId = session['ticketId']
    sessionId = session['sessionId']
    expiration = datetime.fromtimestamp(session['expirationTimeSeconds'])
Originally created by @Tatsh on GitHub (Aug 23, 2017). ## Please follow the guide below - You will be asked some questions and requested to provide some information, please read them **carefully** and answer honestly - Put an `x` into all the boxes [ ] relevant to your *issue* (like this: `[x]`) - Use the *Preview* tab to see what your issue will actually look like --- ### Make sure you are using the *latest* version: run `youtube-dl --version` and ensure your version is *2017.08.23*. If it's not, read [this FAQ entry](https://github.com/rg3/youtube-dl/blob/master/README.md#how-do-i-update-youtube-dl) and update. Issues with outdated version will be rejected. - [x] I've **verified** and **I assure** that I'm running youtube-dl **2017.08.23** ### Before submitting an *issue* make sure you have: - [x] At least skimmed through the [README](https://github.com/rg3/youtube-dl/blob/master/README.md), **most notably** the [FAQ](https://github.com/rg3/youtube-dl#faq) and [BUGS](https://github.com/rg3/youtube-dl#bugs) sections - [x] [Searched](https://github.com/rg3/youtube-dl/search?type=Issues) the bugtracker for similar issues including closed ones ### What is the purpose of your *issue*? - [ ] Bug report (encountered problems with youtube-dl) - [ ] Site support request (request for adding support for a new site) - [ ] Feature request (request for a new functionality) - [x] Question - [x] Other --- ~~Anyone else interested in helping with this please contact me.~~ I have done a bit of research understanding how watching live TV at https://watch.spectrum.net/livetv works and on iOS as well which uses the Adobe SDK. I have not yet completed this research but these are my discoveries: * Authentication does use cookies but not always * Authentication on the API uses OAuth v1 * Streams are encrypted, the metadata is embedded in the m3u8 and it is base64 encoded (PKCS#7 DER encoded inside) * Your OAuth consumer key and secret (which never change) are found in on the main page in the JavaScript: GET https://watch.spectrum.net/livetv with no cookies find line with `window.onload = function () {` find next line with `var environments = `... parse this base64-encoded string. sample data: `[{"name":"prod","label":"production","splunk":{"domain":" https://splunk.ngclogging.cloud.twc.net/"},"analytics":{"endpoint":"https://v-collector.dp.aws.charter.com/api/collector"},"vpns":{"baseUri":"https://vpns-gen.timewarnercable.com"},"oAuth":{"consumerKey":"joeigjioegj","s":"jaoiegjoejg"},"displayRawRentError":false,"default":true}]"` Use this OAuth data to get token Getting the temporary token: GET `https://services.timewarnercable.com/auth/oauth/request` OAuth data required from previous Returns URL-encoded string with xoauth data example: `oauth_token=xxxx&oauth_token_secret=xxx&xoauth_token_expiration=1503113510937&oauth_callback_confirmed=true` 'Authorise' your device: POST https://services.timewarnercable.com/auth/oauth/device/authorize OAuth data required from last step POST data: xoauth_device_id: xxx xoauth_device_type: ONEAPP-OVP oauth_token: xxx username: your spectrum account username (could be an email) password: ... To get a session ID, etc: Oauth GET `https://services.timewarnercable.com/ipvs/api/smarttv/adobe/session` In the JSON: ticketId, sessionId, expiration (UNIX timestamp) Then get stream information, JSON which will have the URI to an m3u8 file: OAuth GET: `https://services.timewarnercable.com/ipvs/api/smarttv/stream/live/v1/172?adID=<adId>&csid=stva_ovp_pc_live&dai-supported=true&drm-supported=true&encoding=hls&sessionId=<your session ID>&vast-supported=true` I have no idea what `adID` is in the above URL. In the JSON, you will see the key `stream_url` which is the full m3u8 URL. This is downloadable without cookies or anything (authorisation is in the query string). It is timed and will eventually expire. In the M3U8, you can find the following: EXT-X-FAXS-CM key -> base64 decode -> pkcs7 decode base64 -d file-with-content > faxs.der openssl pkcs7 -inform der -in faxs.der -print_certs POST URL is in the metadata strings -n 10 faxs.der The POST URL for the license server found in the metadata requires query parameters: `CrmId=twc&AccountId=twc&ContentId=6_ae&SubContentType=Primetime&OriginalUri=/flashaccess/getServerVersion/v3&Ticket=<ticket ID>&SessionId=<SessionId>` `ContentId` is the channel ID (in this case A&E). You must POST a certificate as is (possibly URL encoded which seems strange yes) in the body of the request. What you get back is a 'individualization certificate', as in the decryption key? There are two such requests. I do not know why. They differ in size so they are different. Once the key is grabbed, I assume any TS file can be decrypted. The algorithm is not really known, but I seem to see references to AES-128-CBC in the Adobe Player SDK on iOS. There is a method named: `-[DRMManager initDecryptionSession:playbackSession:error:complete:]`. Python code to demonstrate (set global variables `USERNAME` and `PASSWORD`): ```python from base64 import b64decode from datetime import datetime from math import trunc from random import SystemRandom import json import logging import re import sys from requests_oauthlib import OAuth1Session import requests USER_AGENT = ('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' '(KHTML, like Gecko) Chrome/61.0.3163.49 Safari/537.36') URL_LIVE_TV = 'https://watch.spectrum.net/livetv' # TODO Get URLs dynamically like the site URL_AUTO_AUTHORIZATION = 'https://services.timewarnercable.com/auth/oauth/auto/authorize' URL_DEVICE_AUTHORIZATION = 'https://services.timewarnercable.com/auth/oauth/device/authorize' URL_STATUS = 'https://services.timewarnercable.com/auth/oauth/token/status' URL_TEMPORARY_REQUEST = 'https://services.timewarnercable.com/auth/oauth/request' URL_TOKEN_EXCHANGE = 'https://services.timewarnercable.com/auth/oauth/ssotoken/exchange' URL_TOKEN = 'https://services.timewarnercable.com/auth/oauth/token' URL_WAYFARER = 'https://services.timewarnercable.com/auth/oauth/wayfarer' URL_ADOBE_SESSION = 'https://services.timewarnercable.com/ipvs/api/smarttv/adobe/session' URL_VPNS_CONNECT = 'https://vpns-gen.timewarnercable.com/vpnspush/v1_5/connect' URL_VPNS_REGISTRATION = 'https://vpns-gen.timewarnercable.com/vpnsservice/v1_5/registration' # Device ID original JavaScript: # "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace( # /[xy]/g, # (e) => { # var t = (16 * Math.random()) | 0; # if (e === 'x') { # n = t; # } else { # n = 3 & t | 8; # } # return n.toString(16); # }) def generate_device_id(): rand = SystemRandom() s = [x for x in 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'] for i, c in enumerate(s): if c == 'x' or c == 'y': t = trunc(16 * rand.random()) | 0 if c == 'x': n = t else: n = 3 & t | 8 s[i] = hex(n)[2:] return ''.join(s) if __name__ == '__main__': log = logging.getLogger('oauthlib') log.addHandler(logging.StreamHandler(sys.stdout)) log.setLevel(logging.DEBUG) sess = requests.Session() sess.headers.update({'User-Agent': USER_AGENT}) # GET https://watch.spectrum.net/livetv with no cookies r = sess.get(URL_LIVE_TV) r.raise_for_status() data = None for line in r.content.decode('utf-8').split('\n'): if 'var environments' not in line: continue m = re.match(r'(?:\s+)?var(?:\s+)environments(?:\s+)=(?:\s+)[\'"]([^\'"]+)[\'"]', line) if not m: raise Exception() data = json.loads(b64decode(m.group(1)).decode('utf-8')) if not data: raise Exception() oauth_consumer_key = data[0]['oAuth']['consumerKey'] oauth_secret = b64decode(b64decode(data[0]['oAuth']['s'])) oauth = OAuth1Session(oauth_consumer_key, client_secret=oauth_secret) oauth.headers.update({ 'User-Agent': USER_AGENT, 'Referer': URL_LIVE_TV, 'Origin': 'https://watch.spectrum.net', 'Accept': 'application/json, text/plain, */*', }) token_data = oauth.fetch_request_token(URL_TEMPORARY_REQUEST) device_id = generate_device_id() r = oauth.post(URL_DEVICE_AUTHORIZATION, data=dict( xoauth_device_id=device_id, xoauth_device_type='ONEAPP-OVP', oauth_token=token_data['oauth_token'], username=USERNAME, password=PASSWORD, )) r.raise_for_status() dev_auth = 'https://f?oauth_token={}&{}'.format(token_data['oauth_token'], r.content.decode('utf-8')) dev_auth = oauth.parse_authorization_response(dev_auth) r = oauth.post(URL_TOKEN) r.raise_for_status() oauth_token = 'https://f?{}'.format(r.content.decode('utf-8')) oauth_token = oauth.parse_authorization_response(oauth_token) oauth = OAuth1Session(oauth_consumer_key, client_secret=oauth_secret, resource_owner_key=oauth_token['oauth_token'], resource_owner_secret=oauth_token['oauth_token_secret'], verifier=dev_auth['oauth_verifier']) oauth.headers.update({ 'User-Agent': USER_AGENT, 'Referer': URL_LIVE_TV, 'Origin': 'https://watch.spectrum.net', 'Accept': 'application/json', }) # VPNS - what is VPNS? :( #r = oauth.post(URL_VPNS_REGISTRATION, json={ #'Registration': { #'Device': { #'id': device_id, #}, #'id': device_id, #'operation': 'create', #} #}) #r.raise_for_status() #registration_data = r.json() #vpns_client_id = registration_data['Registration']['Client']['id'] #vpns_session_id = r.headers['X-VPNS-NOTIFY-SESSIONID'] r = oauth.get(URL_ADOBE_SESSION) r.raise_for_status() session = r.json() ticketId = session['ticketId'] sessionId = session['sessionId'] expiration = datetime.fromtimestamp(session['expirationTimeSeconds']) ```
deekerman 2026-02-21 06:13:46 -05:00
Author
Owner

@besweeet commented on GitHub (Nov 7, 2017):

Any progress recently?

You used to be able to download on demand shows via streamlink / livestreamer by simply replacing "HLS_DRM" in the M3U8's URL with "HLS" but that no longer works.

@besweeet commented on GitHub (Nov 7, 2017): Any progress recently? You used to be able to download on demand shows via streamlink / livestreamer by simply replacing "HLS_DRM" in the M3U8's URL with "HLS" but that no longer works.
Author
Owner

@Tatsh commented on GitHub (Nov 8, 2017):

Haven't had a chance to look at this lately.

If anything the work is in debugging Adobe Flash DRM in multiple ways:

  • Decompile the SWF that Spectrum/others use and analyse (I found key mentionings, API endpoints, etc), but this does not really contain decryption code because that's Adobe's code
  • Debug/Disassemble the iOS app and Flash DRM iOS framework with Hopper or IDA Pro (only latest Hopper can make psuedo-code from arm64 code)
  • Debug/Disassemble the DLL/dylib of the Flash browser plugin with Hopper or IDA Pro

Once one is cracked arguably the rest are done too until a Flash update comes. An exploit is definitely another potential and it could be found by disassembling the Flash plugin.

I don't think this DRM scheme has been cracked publicly nor have I seen any content on the web spreading that was from a source that used Adobe's DRM as far as I know. Netflix/Amazon/etc all use their own systems (EME/M3U8+AES key in a browser, their own apps on iOS/Android/etc).

@Tatsh commented on GitHub (Nov 8, 2017): Haven't had a chance to look at this lately. If anything the work is in debugging Adobe Flash DRM in multiple ways: - Decompile the SWF that Spectrum/others use and analyse (I found key mentionings, API endpoints, etc), but this does not really contain decryption code because that's Adobe's code - Debug/Disassemble the iOS app and Flash DRM iOS framework with Hopper or IDA Pro (only latest Hopper can make psuedo-code from arm64 code) - Debug/Disassemble the DLL/dylib of the Flash browser plugin with Hopper or IDA Pro Once one is cracked arguably the rest are done too until a Flash update comes. An exploit is definitely another potential and it could be found by disassembling the Flash plugin. I don't think this DRM scheme has been cracked publicly nor have I seen any content on the web spreading that was from a source that used Adobe's DRM as far as I know. Netflix/Amazon/etc all use their own systems (EME/M3U8+AES key in a browser, their own apps on iOS/Android/etc).
Author
Owner

@yan12125 commented on GitHub (Nov 11, 2017):

The word "FAXS" reminds me of SAMPLE-AES (#9786), which is a different decryption flow than AES-128. There's a Javascript implementation at https://github.com/video-dev/hls.js/pull/997

@yan12125 commented on GitHub (Nov 11, 2017): The word "FAXS" reminds me of SAMPLE-AES (#9786), which is a different decryption flow than AES-128. There's a Javascript implementation at https://github.com/video-dev/hls.js/pull/997
Author
Owner

@Tatsh commented on GitHub (Jan 15, 2018):

I do not know if anyone wants to continue this work, which is not so much Spectrum as it is to figure out Adobe Pass protocol. I do not have a Spectrum account anymore so I cannot do anymore work on this.

@Tatsh commented on GitHub (Jan 15, 2018): I do not know if anyone wants to continue this work, which is not so much Spectrum as it is to figure out Adobe Pass protocol. I do not have a Spectrum account anymore so I cannot do anymore work on this.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/youtube-dl-ytdl-org#11520
No description provided.