+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# Module: MSL
+# Created on: 26.01.2017
+
import base64
import gzip
import json
import pprint
import random
from StringIO import StringIO
-from hmac import HMAC
-import hashlib
+
+from datetime import datetime
import requests
import zlib
import time
-from Crypto.PublicKey import RSA
-from Crypto.Cipher import PKCS1_OAEP
-from Crypto.Cipher import AES
-from Crypto.Random import get_random_bytes
-# from Crypto.Hash import HMAC, SHA256
-from Crypto.Util import Padding
+from Cryptodome.PublicKey import RSA
+from Cryptodome.Cipher import PKCS1_OAEP
+from Cryptodome.Cipher import AES
+from Cryptodome.Random import get_random_bytes
+from Cryptodome.Hash import HMAC, SHA256
+from Cryptodome.Util import Padding
import xml.etree.ElementTree as ET
pp = pprint.PrettyPrinter(indent=4)
last_drm_context = ''
last_playback_context = ''
#esn = "NFCDCH-LX-CQE0NU6PA5714R25VPLXVU2A193T36"
- esn = "WWW-BROWSE-D7GW1G4NPXGR1F0X1H3EQGY3V1F5WE"
+ #esn = "WWW-BROWSE-D7GW1G4NPXGR1F0X1H3EQGY3V1F5WE"
+ #esn = "NFCDIE-02-DCH84Q2EK3N6VFVQJ0NLRQ27498N0F"
current_message_id = 0
session = requests.session()
rndm = random.SystemRandom()
'license': 'http://www.netflix.com/api/msl/NFCDCH-LX/cadmium/license'
}
- def __init__(self, email, password, kodi_helper):
+ def __init__(self, kodi_helper):
"""
The Constructor checks for already existing crypto Keys.
If they exist it will load the existing keys
"""
- self.email = email
- self.password = password
self.kodi_helper = kodi_helper
try:
os.mkdir(self.kodi_helper.msl_data_path)
pass
if self.file_exists(self.kodi_helper.msl_data_path, 'msl_data.json'):
+ self.kodi_helper.log(msg='MSL Data exists. Use old Tokens.')
self.__load_msl_data()
self.handshake_performed = True
elif self.file_exists(self.kodi_helper.msl_data_path, 'rsa_key.bin'):
self.kodi_helper.log(msg='RSA Keys do already exist load old ones')
self.__load_rsa_keys()
- self.__perform_key_handshake()
+ if self.kodi_helper.get_esn():
+ self.__perform_key_handshake()
else:
self.kodi_helper.log(msg='Create new RSA Keys')
# Create new Key Pair and save
self.rsa_key = RSA.generate(2048)
self.__save_rsa_keys()
- self.__perform_key_handshake()
+ if self.kodi_helper.get_esn():
+ self.__perform_key_handshake()
+
+ def perform_key_handshake(self):
+ self.__perform_key_handshake()
def load_manifest(self, viewable_id):
+ """
+ Loads the manifets for the given viewable_id and returns a mpd-XML-Manifest
+ :param viewable_id: The id of of the viewable
+ :return: MPD XML Manifest or False if no success
+ """
manifest_request_data = {
'method': 'manifest',
'lookupType': 'PREPARE',
'viewableIds': [viewable_id],
'profiles': [
- 'playready-h264mpl30-dash',
- 'playready-h264mpl31-dash',
+ "playready-h264bpl30-dash",
+ "playready-h264mpl30-dash",
+ "playready-h264mpl31-dash",
+ "playready-h264mpl40-dash",
+ # "hevc-main-L30-dash-cenc",
+ # "hevc-main-L31-dash-cenc",
+ # "hevc-main-L40-dash-cenc",
+ # "hevc-main-L41-dash-cenc",
+ # "hevc-main-L50-dash-cenc",
+ # "hevc-main-L51-dash-cenc",
+ # "hevc-main10-L30-dash-cenc",
+ # "hevc-main10-L31-dash-cenc",
+ # "hevc-main10-L40-dash-cenc",
+ # "hevc-main10-L41-dash-cenc",
+ # "hevc-main10-L50-dash-cenc",
+ # "hevc-main10-L51-dash-cenc",
+ # "hevc-main10-L30-dash-cenc-prk",
+ # "hevc-main10-L31-dash-cenc-prk",
+ # "hevc-main10-L40-dash-cenc-prk",
+ # "hevc-main10-L41-dash-cenc-prk",
+ # "hevc-main-L30-L31-dash-cenc-tl",
+ # "hevc-main-L31-L40-dash-cenc-tl",
+ # "hevc-main-L40-L41-dash-cenc-tl",
+ # "hevc-main-L50-L51-dash-cenc-tl",
+ # "hevc-main10-L30-L31-dash-cenc-tl",
+ # "hevc-main10-L31-L40-dash-cenc-tl",
+ # "hevc-main10-L40-L41-dash-cenc-tl",
+ # "hevc-main10-L50-L51-dash-cenc-tl",
+ # "hevc-dv-main10-L30-dash-cenc",
+ # "hevc-dv-main10-L31-dash-cenc",
+ # "hevc-dv-main10-L40-dash-cenc",
+ # "hevc-dv-main10-L41-dash-cenc",
+ # "hevc-dv-main10-L50-dash-cenc",
+ # "hevc-dv-main10-L51-dash-cenc",
+ # "hevc-dv5-main10-L30-dash-cenc-prk",
+ # "hevc-dv5-main10-L31-dash-cenc-prk",
+ # "hevc-dv5-main10-L40-dash-cenc-prk",
+ # "hevc-dv5-main10-L41-dash-cenc-prk",
+ # "hevc-dv5-main10-L50-dash-cenc-prk",
+ # "hevc-dv5-main10-L51-dash-cenc-prk",
+ # "hevc-hdr-main10-L30-dash-cenc",
+ # "hevc-hdr-main10-L31-dash-cenc",
+ # "hevc-hdr-main10-L40-dash-cenc",
+ # "hevc-hdr-main10-L41-dash-cenc",
+ # "hevc-hdr-main10-L50-dash-cenc",
+ # "hevc-hdr-main10-L51-dash-cenc",
+ # "hevc-hdr-main10-L30-dash-cenc-prk",
+ # "hevc-hdr-main10-L31-dash-cenc-prk",
+ # "hevc-hdr-main10-L40-dash-cenc-prk",
+ # "hevc-hdr-main10-L41-dash-cenc-prk",
+ # "hevc-hdr-main10-L50-dash-cenc-prk",
+ # "hevc-hdr-main10-L51-dash-cenc-prk"
+
+ # 'playready-h264mpl30-dash',
+ #'playready-h264mpl31-dash',
+ #'playready-h264mpl40-dash',
+ #'hevc-main10-L41-dash-cenc',
+ #'hevc-main10-L50-dash-cenc',
+ #'hevc-main10-L51-dash-cenc',
+
+
+
+ # Audio
'heaac-2-dash',
- 'dfxp-ls-sdh',
+
+ #subtiltes
+ #'dfxp-ls-sdh',
'simplesdh',
- 'nflx-cmisc',
+ #'nflx-cmisc',
+
+ #unkown
'BIF240',
'BIF320'
],
'clientVersion': '4.0004.899.011',
'uiVersion': 'akira'
}
- request_data = self.__generate_msl_request_data(manifest_request_data)
- resp = self.session.post(self.endpoints['manifest'], request_data)
+ # Check if dolby sound is enabled and add to profles
+ if self.kodi_helper.get_dolby_setting():
+ manifest_request_data['profiles'].append('ddplus-2.0-dash')
+ manifest_request_data['profiles'].append('ddplus-5.1-dash')
+ request_data = self.__generate_msl_request_data(manifest_request_data)
+ resp = self.session.post(self.endpoints['manifest'], request_data)
try:
+ # if the json() does not fail we have an error because the manifest response is a chuncked json response
resp.json()
- self.kodi_helper.log(msg='MANIFEST RESPONE JSON: '+resp.text)
+ self.kodi_helper.log(msg='Error getting Manifest: '+resp.text)
+ return False
except ValueError:
- # Maybe we have a CHUNKED response
+ # json() failed so parse the chunked response
+ self.kodi_helper.log(msg='Got chunked Manifest Response: ' + resp.text)
resp = self.__parse_chunked_msl_response(resp.text)
+ self.kodi_helper.log(msg='Parsed chunked Response: ' + json.dumps(resp))
data = self.__decrypt_payload_chunk(resp['payloads'][0])
- # pprint.pprint(data)
return self.__tranform_to_dash(data)
-
def get_license(self, challenge, sid):
-
"""
- std::time_t t = std::time(0); // t is an integer type
- licenseRequestData["clientTime"] = (int)t;
- //licenseRequestData["challengeBase64"] = challengeStr;
- licenseRequestData["licenseType"] = "STANDARD";
- licenseRequestData["playbackContextId"] = playbackContextId;//"E1-BQFRAAELEB32o6Se-GFvjwEIbvDydEtfj6zNzEC3qwfweEPAL3gTHHT2V8rS_u1Mc3mw5BWZrUlKYIu4aArdjN8z_Z8t62E5jRjLMdCKMsVhlSJpiQx0MNW4aGqkYz-1lPh85Quo4I_mxVBG5lgd166B5NDizA8.";
- licenseRequestData["drmContextIds"] = Json::arrayValue;
- licenseRequestData["drmContextIds"].append(drmContextId);
-
- :param viewable_id:
- :param challenge:
- :param kid:
- :return:
+ Requests and returns a license for the given challenge and sid
+ :param challenge: The base64 encoded challenge
+ :param sid: The sid paired to the challengew
+ :return: Base64 representation of the license key or False if no success
"""
-
license_request_data = {
'method': 'license',
'licenseType': 'STANDARD',
resp = self.session.post(self.endpoints['license'], request_data)
try:
+ # If is valid json the request for the licnese failed
resp.json()
- self.kodi_helper.log(msg='LICENSE RESPONE JSON: '+resp.text)
+ self.kodi_helper.log(msg='Error getting license: '+resp.text)
+ return False
except ValueError:
- # Maybe we have a CHUNKED response
+ # json() failed so we have a chunked json response
resp = self.__parse_chunked_msl_response(resp.text)
data = self.__decrypt_payload_chunk(resp['payloads'][0])
- # pprint.pprint(data)
if data['success'] is True:
return data['result']['licenses'][0]['data']
else:
- return ''
-
+ self.kodi_helper.log(msg='Error getting license: ' + json.dumps(data))
+ return False
def __decrypt_payload_chunk(self, payloadchunk):
payloadchunk = json.JSONDecoder().decode(payloadchunk)
if len(manifest['psshb64']) >= 1:
pssh = manifest['psshb64'][0]
-
+ seconds = manifest['runtime']/1000
+ init_length = seconds / 2 * 12 + 20*1000
+ duration = "PT"+str(seconds)+".00S"
root = ET.Element('MPD')
root.attrib['xmlns'] = 'urn:mpeg:dash:schema:mpd:2011'
root.attrib['xmlns:cenc'] = 'urn:mpeg:cenc:2013'
-
-
- seconds = manifest['runtime']/1000
- duration = "PT"+str(seconds)+".00S"
+ root.attrib['mediaPresentationDuration'] = duration
period = ET.SubElement(root, 'Period', start='PT0S', duration=duration)
ET.SubElement(protection, 'cenc:pssh').text = pssh
for downloadable in video_track['downloadables']:
+
+ codec = 'h264'
+ if 'hevc' in downloadable['contentProfile']:
+ codec = 'hevc'
+
+ hdcp_versions = '0.0'
+ for hdcp in downloadable['hdcpVersions']:
+ if hdcp != 'none':
+ hdcp_versions = hdcp
+
rep = ET.SubElement(video_adaption_set, 'Representation',
width=str(downloadable['width']),
height=str(downloadable['height']),
bandwidth=str(downloadable['bitrate']*1024),
- codecs='h264',
+ hdcp=hdcp_versions,
+ nflxContentProfile=str(downloadable['contentProfile']),
+ codecs=codec,
mimeType='video/mp4')
#BaseURL
ET.SubElement(rep, 'BaseURL').text = self.__get_base_url(downloadable['urls'])
# Init an Segment block
- segment_base = ET.SubElement(rep, 'SegmentBase', indexRange="0-60000", indexRangeExact="true")
- ET.SubElement(segment_base, 'Initialization', range='0-60000')
+ segment_base = ET.SubElement(rep, 'SegmentBase', indexRange="0-"+str(init_length), indexRangeExact="true")
+ ET.SubElement(segment_base, 'Initialization', range='0-'+str(init_length))
contentType='audio',
mimeType='audio/mp4')
for downloadable in audio_track['downloadables']:
+ codec = 'aac'
+ print downloadable
+ if downloadable['contentProfile'] == 'ddplus-2.0-dash' or downloadable['contentProfile'] == 'ddplus-5.1-dash':
+ codec = 'ec-3'
+ print "codec is: " + codec
rep = ET.SubElement(audio_adaption_set, 'Representation',
- codecs='aac',
+ codecs=codec,
bandwidth=str(downloadable['bitrate']*1024),
mimeType='audio/mp4')
#BaseURL
ET.SubElement(rep, 'BaseURL').text = self.__get_base_url(downloadable['urls'])
# Index range
- segment_base = ET.SubElement(rep, 'SegmentBase', indexRange="0-60000", indexRangeExact="true")
- ET.SubElement(segment_base, 'Initialization', range='0-60000')
+ segment_base = ET.SubElement(rep, 'SegmentBase', indexRange="0-"+str(init_length), indexRangeExact="true")
+ ET.SubElement(segment_base, 'Initialization', range='0-'+str(init_length))
+
+ # Multiple Adaption Sets for subtiles
+ for text_track in manifest['textTracks']:
+ if 'downloadables' not in text_track or text_track['downloadables'] is None:
+ continue
+ subtiles_adaption_set = ET.SubElement(period, 'AdaptationSet',
+ lang=text_track['bcp47'],
+ codecs='stpp',
+ contentType='text',
+ mimeType='application/ttml+xml')
+ for downloadable in text_track['downloadables']:
+ rep = ET.SubElement(subtiles_adaption_set, 'Representation',
+ nflxProfile=downloadable['contentProfile']
+ )
+ ET.SubElement(rep, 'BaseURL').text = self.__get_base_url(downloadable['urls'])
xml = ET.tostring(root, encoding='utf-8', method='xml')
}
def __generate_msl_request_data(self, data):
+ self.__load_msl_data()
header_encryption_envelope = self.__encrypt(self.__generate_msl_header())
header = {
'headerdata': base64.standard_b64encode(header_encryption_envelope),
# Serialize the given Data
serialized_data = json.dumps(data)
serialized_data = serialized_data.replace('"', '\\"')
- serialized_data = '[{},{"headers":{},"path":"/cbp/cadmium-11","payload":{"data":"' + serialized_data + '"},"query":""}]\n'
+ serialized_data = '[{},{"headers":{},"path":"/cbp/cadmium-13","payload":{"data":"' + serialized_data + '"},"query":""}]\n'
compressed_data = self.__compress_data(serialized_data)
'signature': self.__sign(first_payload_encryption_envelope),
}
-
- # Create Second Payload
- second_payload = {
- "messageid": self.current_message_id,
- "data": "",
- "endofmsg": True,
- "sequencenumber": 2
- }
- second_payload_encryption_envelope = self.__encrypt(json.dumps(second_payload))
- second_payload_chunk = {
- 'payload': base64.standard_b64encode(second_payload_encryption_envelope),
- 'signature': base64.standard_b64encode(self.__sign(second_payload_encryption_envelope)),
- }
-
- request_data = json.dumps(header) + json.dumps(first_payload_chunk) # + json.dumps(second_payload_chunk)
+ request_data = json.dumps(header) + json.dumps(first_payload_chunk)
return request_data
:return: The base64 encoded JSON String of the header
"""
self.current_message_id = self.rndm.randint(0, pow(2, 52))
+ esn = self.kodi_helper.get_esn()
header_data = {
- 'sender': self.esn,
+ 'sender': esn,
'handshake': is_handshake,
'nonreplayable': False,
'capabilities': {
if 'usertoken' in self.tokens:
pass
else:
+ account = self.kodi_helper.get_credentials()
# Auth via email and password
header_data['userauthdata'] = {
'scheme': 'EMAIL_PASSWORD',
'authdata': {
- 'email': self.email,
- 'password': self.password
+ 'email': account['email'],
+ 'password': account['password']
}
}
:param plaintext:
:return: Serialized JSON String of the encryption Envelope
"""
+ esn = self.kodi_helper.get_esn()
+
iv = get_random_bytes(16)
encryption_envelope = {
'ciphertext': '',
- 'keyid': self.esn + '_' + str(self.sequence_number),
+ 'keyid': esn + '_' + str(self.sequence_number),
'sha256': 'AA==',
'iv': base64.standard_b64encode(iv)
}
encryption_envelope['ciphertext'] = base64.standard_b64encode(ciphertext)
return json.dumps(encryption_envelope)
- def __sign(self, text):
- #signature = hmac.new(self.sign_key, text, hashlib.sha256).digest()
- signature = HMAC(self.sign_key, text, hashlib.sha256).digest()
-
- # hmac = HMAC.new(self.sign_key, digestmod=SHA256)
- # hmac.update(text)
+ def __sign(self, text):
+ """
+ Calculates the HMAC signature for the given text with the current sign key and SHA256
+ :param text:
+ :return: Base64 encoded signature
+ """
+ signature = HMAC.new(self.sign_key, text, SHA256).digest()
return base64.standard_b64encode(signature)
def __perform_key_handshake(self):
-
header = self.__generate_msl_header(is_key_request=True, is_handshake=True, compressionalgo="", encrypt=False)
+ esn = self.kodi_helper.get_esn()
+
request = {
'entityauthdata': {
'scheme': 'NONE',
'authdata': {
- 'identity': self.esn
+ 'identity': esn
}
},
'headerdata': base64.standard_b64encode(header),
self.kodi_helper.log(msg='Key Handshake Request:')
self.kodi_helper.log(msg=json.dumps(request))
-
resp = self.session.post(self.endpoints['manifest'], json.dumps(request, sort_keys=True))
if resp.status_code == 200:
resp = resp.json()
def __load_msl_data(self):
msl_data = json.JSONDecoder().decode(self.load_file(self.kodi_helper.msl_data_path, 'msl_data.json'))
+ #Check expire date of the token
+ master_token = json.JSONDecoder().decode(base64.standard_b64decode(msl_data['tokens']['mastertoken']['tokendata']))
+ valid_until = datetime.utcfromtimestamp(int(master_token['expiration']))
+ present = datetime.now()
+ difference = valid_until - present
+ difference = difference.total_seconds() / 60 / 60
+ # If token expires in less then 10 hours or is expires renew it
+ if difference < 10:
+ self.__load_rsa_keys()
+ self.__perform_key_handshake()
+ return
+
self.__set_master_token(msl_data['tokens']['mastertoken'])
self.encryption_key = base64.standard_b64decode(msl_data['encryption_key'])
self.sign_key = base64.standard_b64decode(msl_data['sign_key'])
+
+ def save_msl_data(self):
+ self.__save_msl_data()
+
def __save_msl_data(self):
"""
Saves the keys and tokens in json file
def __set_master_token(self, master_token):
self.mastertoken = master_token
- self.sequence_number = json.JSONDecoder().decode(base64.standard_b64decode(master_token['tokendata']))[
- 'sequencenumber']
+ self.sequence_number = json.JSONDecoder().decode(base64.standard_b64decode(master_token['tokendata']))['sequencenumber']
def __load_rsa_keys(self):
loaded_key = self.load_file(self.kodi_helper.msl_data_path, 'rsa_key.bin')