Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adobe Pass support and research #14004

Closed
Tatsh opened this issue Aug 23, 2017 · 4 comments
Closed

Adobe Pass support and research #14004

Tatsh opened this issue Aug 23, 2017 · 4 comments

Comments

@Tatsh
Copy link
Contributor

@Tatsh Tatsh commented 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'])
@remitamine remitamine added the DRM label Aug 24, 2017
@besweeet
Copy link

@besweeet besweeet commented Nov 8, 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.

@Tatsh
Copy link
Contributor Author

@Tatsh Tatsh commented 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).

@yan12125
Copy link
Collaborator

@yan12125 yan12125 commented 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 video-dev/hls.js#997

@Tatsh
Copy link
Contributor Author

@Tatsh Tatsh commented 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 Tatsh changed the title Spectrum live TV support and research Adobe Pass support and research Jan 15, 2018
@Tatsh Tatsh closed this Nov 23, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
4 participants
You can’t perform that action at this time.