tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

drm-messagehandler.js (11371B)


      1 (function(){
      2 // Expect utf8decoder and utf8decoder to be TextEncoder('utf-8') and TextDecoder('utf-8') respectively
      3 //
      4 // drmconfig format:
      5 // { <keysystem> : {    "serverURL"             : <the url for the server>,
      6 //                      "httpRequestHeaders"    : <map of HTTP request headers>,
      7 //                      "servertype"            : "microsoft" | "drmtoday",                 // affects how request parameters are formed
      8 //                      "certificate"           : <base64 encoded server certificate> } }
      9 //
     10 
     11 drmtodaysecret = Uint8Array.from( [144, 34, 109, 76, 134, 7, 97, 107, 98, 251, 140, 28, 98, 79, 153, 222, 231, 245, 154, 226, 193, 1, 213, 207, 152, 204, 144, 15, 13, 2, 37, 236] );
     12 
     13 drmconfig = {
     14    "com.widevine.alpha": [ {
     15        "serverURL": "https://lic.staging.drmtoday.com/license-proxy-widevine/cenc/",
     16        "servertype" : "drmtoday",
     17        "merchant" : "w3c-eme-test",
     18        "secret" : drmtodaysecret
     19    } ],
     20    "com.microsoft.playready": [ {
     21        "serverURL": "https://lic.staging.drmtoday.com/license-proxy-headerauth/drmtoday/RightsManager.asmx",
     22        "servertype" : "drmtoday",
     23        "sessionTypes" : [ "temporary", "persistent-license" ],
     24        "merchant" : "w3c-eme-test",
     25        "secret" : drmtodaysecret
     26    } ],
     27    "com.microsoft.playready.recommendation": [
     28    {
     29        "serverURL": "https://test.playready.microsoft.com/service/rightsmanager.asmx",
     30        "servertype" : "microsoft",
     31        "sessionTypes" : [ "temporary", "persistent-license" ],
     32        "merchant" : "w3c-eme-test",
     33    } ],
     34 };
     35 
     36 
     37 var keySystemWrappers = {
     38    // Key System wrappers map messages and pass to a handler, then map the response and return to caller
     39    //
     40    // function wrapper(handler, messageType, message, params)
     41    //
     42    // where:
     43    //      Promise<response> handler(messageType, message, responseType, headers, params);
     44    //
     45 
     46    'com.widevine.alpha': function(handler, messageType, message, params) {
     47        return handler.call(this, messageType, new Uint8Array(message), 'json', null, params).then(function(response){
     48            return base64DecodeToUnit8Array(response.license);
     49        });
     50    },
     51 
     52    playReadyHandler : function(handler, messageType, message, params) {
     53        var msg, xmlDoc;
     54        var licenseRequest = null;
     55        var headers = {};
     56        var parser = new DOMParser();
     57        var dataview = new Uint16Array(message);
     58 
     59        msg = String.fromCharCode.apply(null, dataview);
     60        xmlDoc = parser.parseFromString(msg, 'application/xml');
     61 
     62        if (xmlDoc.getElementsByTagName('Challenge')[0]) {
     63            var challenge = xmlDoc.getElementsByTagName('Challenge')[0].childNodes[0].nodeValue;
     64            if (challenge) {
     65                licenseRequest = atob(challenge);
     66            }
     67        }
     68 
     69        var headerNameList = xmlDoc.getElementsByTagName('name');
     70        var headerValueList = xmlDoc.getElementsByTagName('value');
     71        for (var i = 0; i < headerNameList.length; i++) {
     72            headers[headerNameList[i].childNodes[0].nodeValue] = headerValueList[i].childNodes[0].nodeValue;
     73        }
     74        // some versions of the PlayReady CDM return 'Content' instead of 'Content-Type',
     75        // but the license server expects 'Content-Type', so we fix it up here.
     76        if (headers.hasOwnProperty('Content')) {
     77            headers['Content-Type'] = headers.Content;
     78            delete headers.Content;
     79        }
     80 
     81        return handler.call(this, messageType, licenseRequest, 'arraybuffer', headers, params).catch(function(response){
     82            return response.text().then( function( error ) { throw error; } );
     83        });
     84    },
     85 
     86    'com.microsoft.playready': function(handler, messageType, message, params) {
     87        return keySystemWrappers.playReadyHandler.call(this, handler, messageType, message, params);
     88    },
     89 
     90    'com.microsoft.playready.recommendation': function(handler, messageType, message, params) {
     91        return keySystemWrappers.playReadyHandler.call(this, handler, messageType, message, params);
     92    }
     93 };
     94 
     95 const requestConstructors = {
     96    // Server request construction functions
     97    //
     98    // Promise<request> constructRequest(config, sessionType, content, messageType, message, params)
     99    //
    100    // request = { url: ..., headers: ..., body: ... }
    101    //
    102    // content = { assetId: ..., variantId: ..., key: ... }
    103    // params = { expiration: ... }
    104 
    105    'drmtoday': function(config, sessionType, content, messageType, message, headers, params) {
    106        var optData = JSON.stringify({merchant: config.merchant, userId:"12345", sessionId:""});
    107        var crt = {};
    108        if (messageType === 'license-request') {
    109            crt = {assetId: content.assetId,
    110                    outputProtection: {digital : false, analogue: false, enforce: false},
    111                    storeLicense: (sessionType === 'persistent-license')};
    112 
    113            if (!params || (params.expiration === undefined && params.playDuration === undefined)) {
    114                crt.profile = {purchase: {}};
    115            } else {
    116                var expiration = params.expiration || (Date.now().valueOf() + 3600000),
    117                    playDuration = params.playDuration || 3600000;
    118 
    119                crt.profile = {rental: {absoluteExpiration: (new Date(expiration)).toISOString(),
    120                                        playDuration: playDuration } };
    121            }
    122 
    123            if (content.variantId !== undefined) {
    124                crt.variantId = content.variantId;
    125            }
    126        }
    127 
    128        return JWT.encode("HS256", {optData: optData, crt: JSON.stringify([crt])}, config.secret).then(function(jwt){
    129            headers = headers || {};
    130            headers['x-dt-auth-token'] = jwt;
    131            return {url: config.serverURL, headers: headers, body: message};
    132        });
    133    },
    134 
    135    'microsoft': function(config, sessionType, content, messageType, message, headers, params) {
    136        var url = config.serverURL;
    137        if (messageType === 'license-request') {
    138            url += "?";
    139            if (sessionType === 'temporary') {
    140                url += "UseSimpleNonPersistentLicense=1&";
    141            }
    142            url += "PlayEnablers=B621D91F-EDCC-4035-8D4B-DC71760D43E9&";    // disable output protection
    143            url += "ContentKey=" + btoa(String.fromCharCode.apply(null, content.key));
    144        }
    145 
    146        // TODO: Include expiration time in URL
    147        return Promise.resolve({url: url, headers: headers, body: message});
    148    }
    149 };
    150 
    151 MessageHandler = function(keysystem, content, sessionType) {
    152    sessionType = sessionType || "temporary";
    153 
    154    this._keysystem = keysystem;
    155    this._content = content;
    156    this._sessionType = sessionType;
    157    try {
    158        this._drmconfig = drmconfig[this._keysystem].filter(function(drmconfig) {
    159            return drmconfig.sessionTypes === undefined || (drmconfig.sessionTypes.indexOf(sessionType) !== -1);
    160        })[0];
    161        this._requestConstructor = requestConstructors[this._drmconfig.servertype];
    162 
    163        this.messagehandler = keySystemWrappers[keysystem].bind(this, MessageHandler.prototype.messagehandler);
    164 
    165        if (this._drmconfig && this._drmconfig.certificate) {
    166            this.servercertificate = stringToUint8Array(atob(this._drmconfig.certificate));
    167        }
    168    } catch(e) {
    169        return null;
    170    }
    171 }
    172 
    173 MessageHandler.prototype.messagehandler = function messagehandler(messageType, message, responseType, headers, params) {
    174 
    175    var variantId = params ? params.variantId : undefined;
    176    var key;
    177    if( variantId ) {
    178        var keys = this._content.keys.filter(function(k){return k.variantId === variantId;});
    179        if (keys[0]) key = keys[0].key;
    180    }
    181    if (!key) {
    182        key = this._content.keys[0].key;
    183    }
    184 
    185    var content = {assetId:    this._content.assetId,
    186                    variantId:  variantId,
    187                    key:        key};
    188 
    189    return this._requestConstructor(this._drmconfig, this._sessionType, content, messageType, message, headers, params).then(function(request){
    190        return fetch(request.url, {
    191                        method:     'POST',
    192                        headers:    request.headers,
    193                        body:       request.body    });
    194    }).then(function(fetchresponse){
    195        if(fetchresponse.status !== 200) {
    196            throw fetchresponse;
    197        }
    198 
    199        if(responseType === 'json') {
    200            return fetchresponse.json();
    201        } else if(responseType === 'arraybuffer') {
    202            return fetchresponse.arrayBuffer();
    203        }
    204    });
    205 }
    206 
    207 })();
    208 
    209 (function() {
    210 
    211    var subtlecrypto = window.crypto.subtle;
    212 
    213    // Encoding / decoding utilities
    214    function b64pad(b64)        { return b64+"==".substr(0,(b64.length%4)?(4-b64.length%4):0); }
    215    function str2b64url(str)    { return btoa(str).replace(/=+$/g, '').replace(/\+/g, "-").replace(/\//g, "_"); }
    216    function b64url2str(b64)    { return atob(b64pad(b64.replace(/\-/g, "+").replace(/\_/g, "/"))); }
    217    function str2ab(str)        { return Uint8Array.from( str.split(''), function(s){return s.charCodeAt(0)} ); }
    218    function ab2str(ab)         { return String.fromCharCode.apply(null, new Uint8Array(ab)); }
    219 
    220    function jwt2webcrypto(alg) {
    221        if (alg === "HS256") return {name: "HMAC", hash: "SHA-256", length: 256};
    222        else if (alg === "HS384") return { name: "HMAC", hash: "SHA-384", length: 384};
    223        else if (alg === "HS512") return { name: "HMAC", hash: "SHA-512", length: 512};
    224        else throw new Error("Unrecognized JWT algorithm: " + alg);
    225    }
    226 
    227    JWT = {
    228        encode: function encode(alg, claims, secret) {
    229            var algorithm = jwt2webcrypto(alg);
    230            if (secret.byteLength !== algorithm.length / 8) throw new Error("Unexpected secret length: " + secret.byteLength);
    231 
    232            if (!claims.iat) claims.iat = ((Date.now() / 1000) | 0) - 60;
    233            if (!claims.jti) {
    234                var nonce = new Uint8Array(16);
    235                window.crypto.getRandomValues(nonce);
    236                claims.jti = str2b64url( ab2str(nonce) );
    237            }
    238 
    239            var header = {typ: "JWT", alg: alg};
    240            var plaintext = str2b64url(JSON.stringify(header)) + '.' + str2b64url(JSON.stringify(claims));
    241            return subtlecrypto.importKey("raw", secret, algorithm, false, [ "sign" ]).then( function(key) {
    242                return subtlecrypto.sign(algorithm, key, str2ab(plaintext));
    243            }).then(function(hmac){
    244                return plaintext + '.' + str2b64url(ab2str(hmac));
    245            });
    246        },
    247 
    248        decode: function decode(jwt, secret) {
    249            var jwtparts = jwt.split('.');
    250            var header = JSON.parse( b64url2str(jwtparts[0]));
    251            var claims = JSON.parse( b64url2str(jwtparts[1]));
    252            var hmac = str2ab(b64url2str(jwtparts[2]));
    253            var algorithm = jwt2webcrypto(header.alg);
    254            if (secret.byteLength !== algorithm.length / 8) throw new Error("Unexpected secret length: " + secret.byteLength);
    255 
    256            return subtlecrypto.importKey("raw", secret, algorithm, false, ["sign", "verify"]).then(function(key) {
    257                return subtlecrypto.verify(algorithm, key, hmac, str2ab(jwtparts[0] + '.' + jwtparts[1]));
    258            }).then(function(success){
    259                if (!success) throw new Error("Invalid signature");
    260                return claims;
    261            });
    262        }
    263    };
    264 })();