Skip to content

Instantly share code, notes, and snippets.

@almet
Created January 27, 2016 16:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save almet/127efbbcfcfdf0606940 to your computer and use it in GitHub Desktop.
Save almet/127efbbcfcfdf0606940 to your computer and use it in GitHub Desktop.
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