Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
parent
93e3a48004
commit
b38dea0597
25901 changed files with 9982925 additions and 369 deletions
261
gaia/shared/js/omadrm/fl.js
Executable file
261
gaia/shared/js/omadrm/fl.js
Executable file
|
|
@ -0,0 +1,261 @@
|
|||
/* exported ForwardLock */
|
||||
/*
|
||||
* This file defines a ForwardLock object with functions for locking and
|
||||
* unlocking content and for key management for the FirefoxOS implementation
|
||||
* of OMA Forward Lock DRM.
|
||||
*
|
||||
* A locked file has the following structure:
|
||||
*
|
||||
* 6 bytes: ASCII "LOCKED" magic number
|
||||
* 2 bytes: version number as 2 ASCII characters. e.g: " 1"
|
||||
* 1 byte: ASCII space
|
||||
* n bytes: MIME type of the locked content, in ASCII
|
||||
* 1 byte: ASCII \n delimiter
|
||||
* n bytes: other ASCII metadata as key:value pairs, delimited with newlines.
|
||||
* Both key and value passed to JavaScript escape()
|
||||
* 1 byte : 0. NUL terminates the ASCII text, and marks the beginning
|
||||
* of the audio content
|
||||
* rest: The audio content xor'ed with the secret key
|
||||
*
|
||||
* The blob returned by the lock functions shares the first part of its
|
||||
* MIME type ("image/", "audio/", etc.) with the content. But it has a
|
||||
* sub-type of "vnd.mozilla.oma.drm.fl" to indicate that it is locked.
|
||||
*
|
||||
* The secret key used for locking and unlocking content is stored in
|
||||
* the settings database. The security of the key is based entirely on
|
||||
* the fact that only certified apps can read settings. So the secret
|
||||
* is shared among all certified apps.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
(function(exports) {
|
||||
// Make sure we only run once
|
||||
if (exports.ForwardLock) {
|
||||
return;
|
||||
}
|
||||
const mimeSubtype = 'vnd.mozilla.oma.drm.fl';
|
||||
const SECRET_SETTINGS_ID = 'oma.drm.forward_lock.secret.key';
|
||||
var secret = null;
|
||||
|
||||
|
||||
// XOR the buffer contents with the key.
|
||||
// This is pretty fast, but maybe using asm.js could make it faster.
|
||||
function xor(buffer, key) {
|
||||
// View the array buffer as 32-bit words. There may be 1 to 3
|
||||
// bytes at the end of the buffer that are not encrypted.
|
||||
var words = new Uint32Array(buffer, 0, buffer.byteLength >> 2);
|
||||
for (var i = 0, n = words.length; i < n; i++) {
|
||||
words[i] ^= key;
|
||||
}
|
||||
}
|
||||
|
||||
// Return a blob in .lcka format for specified audio content,
|
||||
// metadata and secret key. Key is a 4-byte integer. content is an
|
||||
// ArrayBuffer holding the file content, and metadata is an object
|
||||
// that can be serialized with JSON.stringify.
|
||||
function lockBuffer(secret, content, type, metadata) {
|
||||
var header = 'LOCKED 1 ' + escape(type) + '\n';
|
||||
if (metadata) {
|
||||
for (var p in metadata) {
|
||||
header += escape(p) + ':' + escape(metadata[p]) + '\n';
|
||||
}
|
||||
}
|
||||
header += '\0';
|
||||
|
||||
// Make a private copy of the buffer so we don't modify the caller's copy.
|
||||
var buffer = new Uint8Array(new Uint8Array(content)).buffer;
|
||||
|
||||
// Perform the encryption
|
||||
xor(buffer, secret);
|
||||
|
||||
// And return the blob with a custom MIME type
|
||||
return new Blob([header, buffer], {
|
||||
type: type.split('/')[0] + '/' + mimeSubtype
|
||||
});
|
||||
}
|
||||
|
||||
// Given the secret key the unlocked blob, and some metadata,
|
||||
// create a locked blob and pass it to the callback
|
||||
function lockBlob(secret, blob, metadata, callback) {
|
||||
var reader = new FileReader();
|
||||
reader.readAsArrayBuffer(blob);
|
||||
reader.onload = function() {
|
||||
callback(lockBuffer(secret, reader.result, blob.type, metadata));
|
||||
};
|
||||
}
|
||||
|
||||
// Given a .lcka blob, and the key used to encrypt it, parse the
|
||||
// metadata and decrypt the content and pass them to the specified
|
||||
// callback. If the input blob is not in the right format, call
|
||||
// errorCallback with an string message.
|
||||
function unlockBlob(secret, blob, callback, errorCallback) {
|
||||
if (!secret) {
|
||||
return error('no secret supplied');
|
||||
}
|
||||
|
||||
var reader = new FileReader();
|
||||
reader.readAsArrayBuffer(blob);
|
||||
reader.onload = function() {
|
||||
var buffer = reader.result;
|
||||
var bytes = new Uint8Array(buffer);
|
||||
var header = '';
|
||||
var contentStart;
|
||||
for (var i = 0; i < bytes.length; i++) {
|
||||
if (bytes[i] === 0) {
|
||||
contentStart = i + 1;
|
||||
break;
|
||||
}
|
||||
header += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
|
||||
if (!header.startsWith('LOCKED')) {
|
||||
return error('Bad magic number');
|
||||
}
|
||||
if (header.substring(6, 9) !== ' 1 ') {
|
||||
return error('Unsupported version number');
|
||||
}
|
||||
if (!contentStart) {
|
||||
return error('No content');
|
||||
}
|
||||
var eol = header.indexOf('\n');
|
||||
if (eol === -1) {
|
||||
return error('malformed header');
|
||||
}
|
||||
var type = unescape(header.substring(9, eol).trim());
|
||||
|
||||
var metadata = {};
|
||||
var lines = header.substring(eol + 1).split('\n');
|
||||
for (var j = 0; j < lines.length; j++) {
|
||||
var line = lines[j];
|
||||
if (!line) { // ignore blank line at the end of the key:value pairs
|
||||
continue;
|
||||
}
|
||||
var [key, value] = line.split(':');
|
||||
if (!key || !value) {
|
||||
return error('malformed metadata');
|
||||
}
|
||||
metadata[unescape(key)] = unescape(value);
|
||||
}
|
||||
|
||||
var content = buffer.slice(contentStart);
|
||||
// Decryption is the same as encryption
|
||||
xor(content, secret);
|
||||
var blob = new Blob([content], { type: type });
|
||||
|
||||
try {
|
||||
callback(blob, metadata);
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Exception in content callback', e);
|
||||
}
|
||||
};
|
||||
|
||||
function error(msg) {
|
||||
msg = 'LCKA.decrypt(): ' + msg;
|
||||
if (errorCallback) {
|
||||
try {
|
||||
errorCallback(msg);
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Exception in error callback', e);
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.error(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we already know the secret key, pass it directly to the callback.
|
||||
// Otherwise, look it up in the settings database and call the callback
|
||||
// asynchronously. If the key does not exist in the db, pass null.
|
||||
// This requires read permission for the settings database.
|
||||
// Only certified apps can have that permission
|
||||
function getKey(callback) {
|
||||
try {
|
||||
if (secret !== null) {
|
||||
report(secret);
|
||||
return;
|
||||
}
|
||||
|
||||
var lock = navigator.mozSettings.createLock();
|
||||
var getreq = lock.get(SECRET_SETTINGS_ID);
|
||||
getreq.onsuccess = function() {
|
||||
secret = getreq.result[SECRET_SETTINGS_ID] || null;
|
||||
report(secret);
|
||||
};
|
||||
getreq.onerror = function() {
|
||||
console.error('Error getting ForwardLock setting', getreq.error);
|
||||
report(null);
|
||||
};
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Exception in ForwardLock.getKey():', e);
|
||||
report(null);
|
||||
}
|
||||
|
||||
function report(secret) {
|
||||
if (callback) {
|
||||
try {
|
||||
callback(secret);
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Exception in ForwardLock.getKey() callback', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This function works like getKey, but if the key does not exist, it
|
||||
// is created. This requires write access to the settings DB. The intent
|
||||
// is that only the Locked Content app will ever call this
|
||||
function getOrCreateKey(callback) {
|
||||
getKey(function(key) {
|
||||
if (key !== null) {
|
||||
report(key);
|
||||
return;
|
||||
}
|
||||
|
||||
// The key does not exist yet so create one that is not zero.
|
||||
secret = ((Math.random() * 0xFFFFFFFF) | 0) + 1;
|
||||
|
||||
// Now attempt to save the key to the settings db.
|
||||
var setting = {};
|
||||
setting[SECRET_SETTINGS_ID] = secret;
|
||||
var setreq = navigator.mozSettings.createLock().set(setting);
|
||||
setreq.onsuccess = function() {
|
||||
// If we have permission to write to the settings db, then
|
||||
// the key is now saved for other apps to read
|
||||
report(secret);
|
||||
};
|
||||
setreq.onerror = function() {
|
||||
// Only apps with correct permissions should call this function
|
||||
// so this should not happen. We don't call the callback if
|
||||
// this happens because we want to fail early in this case.
|
||||
console.error('Failed to set key in ForwardLock.getOrCreateKey()',
|
||||
setreq.error.name);
|
||||
secret = null;
|
||||
};
|
||||
});
|
||||
|
||||
function report(secret) {
|
||||
if (callback) {
|
||||
try {
|
||||
callback(secret);
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Exception in ForwardLock.getOrCreateKey() callback');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.ForwardLock = {
|
||||
lockBuffer: lockBuffer,
|
||||
lockBlob: lockBlob,
|
||||
unlockBlob: unlockBlob,
|
||||
mimeSubtype: mimeSubtype,
|
||||
getKey: getKey,
|
||||
getOrCreateKey: getOrCreateKey
|
||||
};
|
||||
}(window));
|
||||
Loading…
Reference in a new issue