Created
January 27, 2016 16:43
-
-
Save almet/127efbbcfcfdf0606940 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function main() { | |
const headers = {Authorization: "Basic " + btoa("user:pass")}; | |
/** | |
* Convert an Array Buffer into a base64 String. | |
* | |
* @param {ArrayBuffer} buffer - The Array buffer to convert. | |
* @returns {String} - The base64 representation of the given Array | |
* Buffer. | |
**/ | |
function arrayBufferToBase64(buffer) { | |
var binary = ''; | |
var bytes = new Uint8Array( buffer ); | |
var len = bytes.byteLength; | |
for (var i = 0; i < len; i++) { | |
binary += String.fromCharCode( bytes[ i ] ); | |
} | |
return window.btoa(binary); | |
} | |
/** | |
* Convert a base64 String into an Array Buffer. | |
* | |
* @param {String} base64 - A base64 string. | |
* @returns {ArrayBuffer} - The Array Buffer representation of the given | |
* string. | |
**/ | |
function base64ToArrayBuffer(base64) { | |
var binary_string = window.atob(base64); | |
var len = binary_string.length; | |
var bytes = new Uint8Array( len ); | |
for (var i = 0; i < len; i++) { | |
bytes[i] = binary_string.charCodeAt(i); | |
} | |
return bytes.buffer; | |
} | |
function sortObjectKeys(node) { | |
if (node === undefined) { | |
return undefined; | |
} else if (typeof node !== "object" || node === null) { | |
return node; | |
} else if (Array.isArray(node)) { | |
const out = []; | |
node.forEach(function(item) { | |
out.push(sortObjectKeys(item)); | |
}); | |
return out; | |
} else { | |
// We need to handle objects. | |
const sortedKeys = []; | |
Object.keys(node).forEach(function(key) { | |
sortedKeys.push(key); | |
}); | |
sortedKeys.sort(); | |
const newObject = {}; | |
sortedKeys.forEach(function(key) { | |
if (node.hasOwnProperty(key)) { | |
newObject[key] = sortObjectKeys(node[key]); | |
} | |
}); | |
return newObject; | |
} | |
} | |
function getCanonicalJSON(records) { | |
// Remove all protected keys of all records. | |
const cleanRecords = records.map(function(record) { | |
const newRecord = {}; | |
Object.keys(record).forEach(function(key) { | |
if (key.indexOf("_") !== 0) { | |
newRecord[key] = record[key]; | |
} | |
}); | |
return newRecord; | |
}).filter(function(record) { | |
return !(record.hasOwnProperty("deleted") && | |
record.deleted === true); | |
}).map(function(record) { | |
return sortObjectKeys(record); | |
}).sort(function(a, b) { | |
if (a.id < b.id) { | |
return -1; | |
} | |
if (a.id > b.id) { | |
return 1; | |
} | |
return 0; | |
}); | |
return JSON.stringify(cleanRecords); | |
} | |
function mergeChanges(localRecords, changes) { | |
const records = {}; | |
localRecords.data.forEach(function(record) { | |
records[record.id] = record; | |
}); | |
// Apply remote changes in the "records" variable. | |
// All records that exists already are replaced by the version from the | |
// server. | |
changes.forEach(function(record) { | |
records[record.id] = record; | |
}); | |
return Object.keys(records).map(function(key){ return records[key]; }); | |
} | |
/** | |
* Compute the hash locally. | |
**/ | |
function computeHash(records) { | |
const canonicalJSON = getCanonicalJSON(records); | |
console.log(canonicalJSON); | |
return window.crypto.subtle.digest( | |
{name: "SHA-256"}, | |
new TextEncoder("utf-8").encode(canonicalJSON) | |
); | |
} | |
function loadKey() { | |
return fetch("/key.pem") | |
.then(function(resp) { | |
return resp.text(); | |
}) | |
.then(function(b64Key) { | |
const publicKey = base64ToArrayBuffer(b64Key); | |
return window.crypto.subtle.importKey( | |
"spki", publicKey, { | |
name: "ECDSA", | |
namedCurve: "P-384" | |
}, | |
false, | |
["verify"] | |
); | |
}); | |
} | |
function verifySignature(publicKey, signature, hash) { | |
console.log("signature", signature); | |
return window.crypto.subtle.verify( | |
{ | |
name: "ECDSA", | |
hash: {name: "SHA-256"}, | |
}, | |
publicKey, | |
base64ToArrayBuffer(signature), | |
hash | |
); | |
} | |
function validateCollectionSignature(payload, collection) { | |
return collection.list() | |
.then(function(localRecords) { | |
return mergeChanges(localRecords, payload.changes.changes); | |
}) | |
.then(computeHash) | |
.then(function(hash) { | |
console.log("The hash is", arrayBufferToBase64(hash)); | |
const collectionURL = collection.api.endpoints().collection( | |
collection._bucket, | |
collection.name | |
); | |
return fetch(collectionURL, { | |
headers: headers | |
}).then(function(resp) { | |
return resp.json(); | |
}) | |
.then(function(json) { | |
console.log("json", json); | |
return json.data.signature; | |
}) | |
.then(function(signature) { | |
return loadKey() | |
.then(function(publicKey) { | |
return { | |
signature: signature, | |
publicKey: publicKey | |
}; | |
}); | |
}) | |
.then(function(data) { | |
return verifySignature(data.publicKey, data.signature, hash); | |
}); | |
}).then(function() { | |
// In case the hash is valid, we don't want to alter the payload, | |
// so just return the same payload we've got in the input. | |
return payload; | |
}); | |
} | |
var db = new Kinto({ | |
remote: "http://alexis-kinto.herokuapp.com/v1/", | |
headers: headers, | |
}); | |
var tasks = db.collection("tasks", { | |
hooks: { | |
"incoming-changes": [validateCollectionSignature] | |
} | |
}); | |
var syncOptions = {}; | |
document.getElementById("form") | |
.addEventListener("submit", function(event) { | |
event.preventDefault(); | |
tasks.create({ | |
title: event.target.title.value, | |
done: false | |
}) | |
.then(function(res) { | |
event.target.title.value = ""; | |
event.target.title.focus(); | |
}) | |
.then(render) | |
.catch(function(err) { | |
console.error(err); | |
}); | |
}); | |
document.getElementById("clearCompleted") | |
.addEventListener("click", function(event) { | |
event.preventDefault(); | |
tasks.list() | |
.then(function(res) { | |
var completed = res.data.filter(function(task) { | |
return task.done; | |
}); | |
return Promise.all(completed.map(function(task) { | |
return tasks.delete(task.id); | |
})); | |
}) | |
.then(render) | |
.catch(function(err) { | |
console.error(err); | |
}); | |
}); | |
function doSync() { | |
return tasks.sync(syncOptions).catch(function(err) { | |
if (err.message.contains("flushed")) { | |
console.warn("Flushed server detected, marking local data for reupload."); | |
return tasks.resetSyncStatus() | |
.then(function() { | |
return tasks.sync(syncOptions); | |
}); | |
} | |
throw err; | |
}); | |
} | |
function handleConflicts(conflicts) { | |
return Promise.all(conflicts.map(function(conflict) { | |
return tasks.resolve(conflict, conflict.remote); | |
})) | |
.then(doSync); | |
} | |
document.getElementById("sync") | |
.addEventListener("click", function(event) { | |
event.preventDefault(); | |
doSync() | |
.then(function(res) { | |
document.getElementById("results").value = JSON.stringify(res, null, 2); | |
if (res.conflicts.length) { | |
return handleConflicts(res.conflicts); | |
} | |
return res; | |
}) | |
.then(render) | |
.catch(function(err) { | |
console.error(err); | |
}); | |
}); | |
function renderTask(task) { | |
var tpl = document.getElementById("task-tpl"); | |
var li = tpl.content.cloneNode(true); | |
li.querySelector(".title").textContent = task.title; | |
li.querySelector(".uuid").textContent = task.id; | |
// retrieve a reference to the checkbox element | |
var checkbox = li.querySelector(".done"); | |
// initialize it with task status | |
checkbox.checked = task.done; | |
// listen to cliecks | |
checkbox.addEventListener("click", function(event) { | |
// prevent the click to actually toggle the checkbox | |
event.preventDefault(); | |
// invert the task status | |
task.done = !task.done; | |
// update task status | |
tasks.update(task) | |
.then(render) | |
.catch(function(err) { | |
console.error(err); | |
}); | |
}); | |
return li; | |
} | |
function renderTasks(tasks) { | |
var ul = document.getElementById("tasks"); | |
ul.innerHTML = ""; | |
tasks.forEach(function(task) { | |
ul.appendChild(renderTask(task)); | |
}); | |
} | |
function render() { | |
tasks.list().then(function(res) { | |
renderTasks(res.data); | |
}).catch(function(err) { | |
console.error(err); | |
}); | |
} | |
render(); | |
} | |
window.addEventListener("DOMContentLoaded", main); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment