mirror of
https://github.com/cryptpad/cryptpad.git
synced 2026-09-14 11:05:41 +05:00
First experimental subresource-integrity signing
This commit is contained in:
parent
0c8ce27f24
commit
5607094429
1
customize.dist/manifest.js
Normal file
1
customize.dist/manifest.js
Normal file
File diff suppressed because one or more lines are too long
1
customize.dist/version.txt
Normal file
1
customize.dist/version.txt
Normal file
@ -0,0 +1 @@
|
||||
I1M7/VtyN5OGut+vhPCtxW2sxbuyZ8vWkoORdeqG6DD4hY7HutC3iyYPJkk6G+m6vhgQPRQqMdUNIzCuFoIPAls4LCJKMkp5c0l2QmRWZFBweVdWSWNvYkc0SXJIZWozYmU0a3lhWDV3UE1ORFlnPSJd
|
||||
115
gen-manifest.js
Normal file
115
gen-manifest.js
Normal file
@ -0,0 +1,115 @@
|
||||
/* jshint esversion: 6 */
|
||||
/* global Buffer */
|
||||
const Fs = require('fs');
|
||||
const nThen = require('nthen');
|
||||
const Crypto = require('crypto');
|
||||
const Nacl = require('tweetnacl');
|
||||
//const SigKey = require('~/.cryptpad_signing_key.json');
|
||||
|
||||
const KEY_FILE = process.env.HOME + '/.cryptpad_signing_key.json';
|
||||
const MANIFEST_FILE = './customize.dist/manifest.js';
|
||||
const VERSION_FILE = './customize.dist/version.txt';
|
||||
|
||||
const nameMapper = (name) => {
|
||||
if (name.startsWith('./www/')) {
|
||||
return name.replace(/^\.\/www\//, '');
|
||||
}
|
||||
if (name.startsWith('./customize.dist/')) {
|
||||
return name.replace(/^\.\/customize.dist\//, 'customize/');
|
||||
}
|
||||
throw new Error();
|
||||
};
|
||||
|
||||
const PUBLIC_KEY = "MYaWgwAcOHIp3sZFGXeWsQX3u7U8PZrqIDaM2jNhXWY=";
|
||||
|
||||
const validate = (cb) => {
|
||||
const key = Nacl.util.decodeBase64(PUBLIC_KEY);
|
||||
Fs.readFile(VERSION_FILE, 'utf8', (err, ret) => {
|
||||
if (err) { throw err; }
|
||||
const buf = Nacl.util.decodeBase64(ret);
|
||||
const data = Nacl.sign.open(buf, key);
|
||||
cb(new Buffer(data).toString('utf8'));
|
||||
});
|
||||
};
|
||||
|
||||
const release = () => {
|
||||
const key = Nacl.sign.keyPair.fromSeed(new Buffer(require(KEY_FILE).seed, 'hex'));
|
||||
const files = [];
|
||||
const fileHashes = {};
|
||||
let manifestHash;
|
||||
let lastVersion = 0;
|
||||
nThen((w) => {
|
||||
const recurse = (dir) => {
|
||||
Fs.readdir(dir, w((err, ret) => {
|
||||
if (err) { throw err; }
|
||||
ret.forEach((_f) => {
|
||||
const f = dir + '/' + _f;
|
||||
Fs.stat(f, w((err, stat) => {
|
||||
if (err) { throw err; }
|
||||
if (stat.isDirectory()) {
|
||||
//console.log('DIR ', f);
|
||||
recurse(f);
|
||||
} else if (/\.js$/.test(f)) {
|
||||
//console.log('FILE', f);
|
||||
files.push(f);
|
||||
}
|
||||
}));
|
||||
});
|
||||
}));
|
||||
};
|
||||
recurse('./www');
|
||||
recurse('./customize.dist');
|
||||
}).nThen((w) => {
|
||||
let nt = nThen;
|
||||
files.forEach((f) => {
|
||||
nt = nt((w) => {
|
||||
Fs.readFile(f, w((err, ret) => {
|
||||
if (err) { throw err; }
|
||||
fileHashes[f] = Crypto.createHash('sha256').update(ret).digest('base64');
|
||||
}));
|
||||
}).nThen;
|
||||
});
|
||||
nt(w());
|
||||
}).nThen((w) => {
|
||||
const manifest = { files: {} };
|
||||
Object.keys(fileHashes).forEach((k) => {
|
||||
let obj = manifest.files;
|
||||
const elems = nameMapper(k).split('/');
|
||||
const jsf = elems.pop();
|
||||
elems.forEach((ke) => { obj = obj[ke] = obj[ke] || {}; });
|
||||
obj[jsf] = fileHashes[k];
|
||||
});
|
||||
Fs.writeFile(MANIFEST_FILE, 'defineManifest(' + JSON.stringify(manifest) + ');', w((err) => {
|
||||
if (err) { throw err; }
|
||||
}));
|
||||
}).nThen((w) => {
|
||||
Fs.readFile(MANIFEST_FILE, w((err, ret) => {
|
||||
if (err) { throw err; }
|
||||
manifestHash = Crypto.createHash('sha256').update(ret).digest('base64');
|
||||
}));
|
||||
}).nThen((w) => {
|
||||
validate(w((ver) => { lastVersion = JSON.parse(ver)[0]; }));
|
||||
}).nThen((w) => {
|
||||
const data = [ lastVersion + 1, manifestHash ];
|
||||
const sig = Nacl.sign(new Buffer(JSON.stringify(data), 'utf8'), key.secretKey);
|
||||
data.push(sig);
|
||||
Fs.writeFile(VERSION_FILE, new Buffer(sig).toString('base64'), w((err) => {
|
||||
if (err) { throw err; }
|
||||
console.log('Saved!');
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
const keypair = () => {
|
||||
if (Fs.exists(KEY_FILE)) { throw new Error(); }
|
||||
const data = JSON.stringify({ seed: Crypto.randomBytes(32).toString('hex') });
|
||||
Fs.writeFile(KEY_FILE, data, (err) => {
|
||||
if (err) { throw err; }
|
||||
console.log('Generated');
|
||||
});
|
||||
//console.log(Nacl.sign.keyPair());
|
||||
};
|
||||
|
||||
//keypair();
|
||||
release();
|
||||
//validate();
|
||||
26
server.js
26
server.js
@ -156,6 +156,32 @@ if (config.privKeyAndCertFiles) {
|
||||
};
|
||||
}
|
||||
|
||||
// TODO race
|
||||
let version;
|
||||
Fs.readFile('./customize.dist/version.txt', 'utf8', (err, v) => {
|
||||
if (err) { throw err; }
|
||||
version = v;
|
||||
});
|
||||
app.get('/api/config.json', function (req, res) {
|
||||
const host = req.headers.host.replace(/\:[0-9]+/, '');
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.send(JSON.stringify({
|
||||
requireConf: {
|
||||
waitSeconds: 60,
|
||||
urlArgs: 'ver=' + Package.version + (FRESH_KEY? '-' + FRESH_KEY: '') + (DEV_MODE? '-' + (+new Date()): ''),
|
||||
},
|
||||
removeDonateButton: (config.removeDonateButton === true),
|
||||
allowSubscriptions: (config.allowSubscriptions === true),
|
||||
websocketPath: config.useExternalWebsocket ? undefined : config.websocketPath,
|
||||
websocketURL:'ws' + ((useSecureWebsockets) ? 's' : '') + '://' + host + ':' +
|
||||
websocketPort + '/cryptpad_websocket',
|
||||
httpUnsafeOrigin: config.httpUnsafeOrigin,
|
||||
httpSafeOrigin: config.httpSafeOrigin || null,
|
||||
httpSafePort: config.httpSafePort || null,
|
||||
versionSig: version
|
||||
}));
|
||||
});
|
||||
|
||||
app.get('/api/config', function(req, res){
|
||||
var host = req.headers.host.replace(/\:[0-9]+/, '');
|
||||
res.setHeader('Content-Type', 'text/javascript');
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
// This is stage 1, it can be changed but you must bump the version of the project.
|
||||
define([
|
||||
'/common/requireconfig.js'
|
||||
], function (RequireConfig) {
|
||||
'/common/requireconfig.js',
|
||||
'/api/config'
|
||||
], function (RequireConfig, ApiConfig) {
|
||||
require.config(RequireConfig());
|
||||
|
||||
// most of CryptPad breaks if you don't support isArray
|
||||
@ -26,6 +27,14 @@ define([
|
||||
Number.MAX_SAFE_INTEGER = 9007199254740991;
|
||||
}
|
||||
|
||||
if (!ApiConfig.httpSafeOrigin) {
|
||||
if (ApiConfig.httpSafePort) {
|
||||
ApiConfig.httpSafeOrigin =
|
||||
window.location.origin.replace(/\:[0-9]+$/, ':' + ApiConfig.httpSafePort);
|
||||
}
|
||||
ApiConfig.httpSafeOrigin = window.location.origin;
|
||||
}
|
||||
|
||||
var failStore = function () {
|
||||
console.error(new Error('wut'));
|
||||
require(['jquery'], function ($) {
|
||||
|
||||
83
www/common/sboot.js
Normal file
83
www/common/sboot.js
Normal file
@ -0,0 +1,83 @@
|
||||
/*@flow*/
|
||||
// The hash of this file is stored directly in the html
|
||||
// YOU MUST NOT CHANGE THIS FILE
|
||||
(function () {
|
||||
var PUBLIC_KEY = "MYaWgwAcOHIp3sZFGXeWsQX3u7U8PZrqIDaM2jNhXWY=";
|
||||
var NACL_HASH = "kcvFQQilxR1viAWvO4PrBTfQylz8bJTlYTpKQ6XeGtw=";
|
||||
var load = function (url, cb) {
|
||||
var req = new XMLHttpRequest();
|
||||
req.onreadystatechange = function () {
|
||||
if (this.readyState === 4 && this.status === 200) { cb(req.responseText); }
|
||||
};
|
||||
req.open("GET", url);
|
||||
req.send();
|
||||
};
|
||||
var script = function (path, hash, done) {
|
||||
var scr = document.createElement('script');
|
||||
scr.async = true;
|
||||
scr.charset = 'utf-8';
|
||||
scr.type = 'text/javascript';
|
||||
scr.src = path;
|
||||
if (done) { scr.addEventListener('load', done, false); }
|
||||
scr.setAttribute('integrity', 'sha256-' + hash);
|
||||
document.getElementsByTagName('head')[0].appendChild(scr);
|
||||
};
|
||||
var ConfigS;
|
||||
var Config;
|
||||
var Nacl;
|
||||
var three = function (manifest) {
|
||||
delete window.defineManifest;
|
||||
var requireHash = manifest.files.bower_components.requirejs['require.js'];
|
||||
script(
|
||||
'/bower_components/requirejs/require.js?ver=' + encodeURIComponent(requireHash),
|
||||
requireHash,
|
||||
function () {
|
||||
var rcfg = Config.requireConf || {};
|
||||
rcfg.onNodeCreated = function (node /*, config, module, path*/) {
|
||||
var src = node.getAttribute('src');
|
||||
var hash = manifest.files;
|
||||
var path = src.replace(/\?.*$/, '').split('/');
|
||||
path.shift(); // leading /
|
||||
for (var i = 0; i < path.length; i++) {
|
||||
var next = hash[path[i]];
|
||||
if (!next) { throw new Error("no entry in manifest for " + src); }
|
||||
hash = next;
|
||||
}
|
||||
if (typeof(hash) !== 'string') { throw new Error("no entry in manifest for " + src); }
|
||||
var newSrc = src.replace(/([\?\&])ver=[^\?\&]*([\?\&])?/, function (all, begin, end) {
|
||||
return begin + 'ver=' + encodeURIComponent(hash) + (end || '');
|
||||
});
|
||||
node.setAttribute('src', newSrc);
|
||||
node.setAttribute('integrity', 'sha256-' + hash);
|
||||
};
|
||||
window.require.config(Config.requireConf);
|
||||
window.define('/api/config', function () { return JSON.parse(ConfigS); });
|
||||
window.require(['/common/boot2.js']);
|
||||
}
|
||||
);
|
||||
};
|
||||
var two = function () {
|
||||
if (!(Nacl && ConfigS)) { return; }
|
||||
Config = JSON.parse(ConfigS);
|
||||
var key = Nacl.util.decodeBase64(PUBLIC_KEY);
|
||||
var buf = Nacl.util.decodeBase64(Config.versionSig);
|
||||
var opened = Nacl.sign.open(buf, key);
|
||||
var openedS = Nacl.util.encodeUTF8(opened);
|
||||
var data = JSON.parse(openedS);
|
||||
var lastVer = Number(localStorage.CRYPTPAD_SBOOT_VERSION || -1);
|
||||
if (!lastVer) {
|
||||
} else if (lastVer > data[0]) {
|
||||
window.alert("Cryptpad secure bootloader failed because version number is lower than last version");
|
||||
throw new Error(lastVer + " > " + data[0]);
|
||||
}
|
||||
localStorage['CRYPTPAD_SBOOT_VERSION'] = data[0];
|
||||
window.defineManifest = three;
|
||||
script("/customize/manifest.js?ver=" + encodeURIComponent(data[1]), data[1]);
|
||||
};
|
||||
script(
|
||||
'/bower_components/tweetnacl/nacl-fast.min.js?ver=' + encodeURIComponent(NACL_HASH),
|
||||
NACL_HASH,
|
||||
function () { Nacl = window.nacl; delete window.nacl; two(); }
|
||||
);
|
||||
load("/api/config.json?ver=" + (+new Date()), function (v) { ConfigS = v; two(); });
|
||||
}());
|
||||
@ -5,7 +5,7 @@
|
||||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="referrer" content="no-referrer" />
|
||||
<script async data-bootload="/common/sframe-app-outer.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
|
||||
<script async data-bootload="/common/sframe-app-outer.js" src="/common/sboot.js"></script>
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0px;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user