405 lines
12 KiB
JavaScript
405 lines
12 KiB
JavaScript
|
/*
|
||
|
* Copyright 2020 Vladimir Panteleev
|
||
|
*
|
||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
* you may not use this file except in compliance with the License.
|
||
|
* You may obtain a copy of the License at
|
||
|
*
|
||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||
|
*
|
||
|
* Unless required by applicable law or agreed to in writing, software
|
||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
* See the License for the specific language governing permissions and
|
||
|
* limitations under the License.
|
||
|
*/
|
||
|
|
||
|
/* jshint esversion : 8, laxbreak:true */
|
||
|
|
||
|
function isKeygenNativelySupported() {
|
||
|
// TODO: iOS - https://github.com/Modernizr/Modernizr/issues/1075
|
||
|
var keygen = document.createElement('keygen');
|
||
|
if ('challenge' in keygen)
|
||
|
return 'blink';
|
||
|
|
||
|
var template = document.createElement('div');
|
||
|
template.innerHTML = '<form><keygen class="keygen"></form>';
|
||
|
keygen = template.getElementsByClassName('keygen')[0];
|
||
|
if (keygen.tagName.toLowerCase() === 'select')
|
||
|
return 'gecko';
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
function keygenJS(OpenSSL) {
|
||
|
if (isKeygenNativelySupported())
|
||
|
return;
|
||
|
|
||
|
const supportedAlgorithms = [ 'RSA', 'RSA-PSS', 'X25519',
|
||
|
'X448', 'ED25519', 'ED448' ];
|
||
|
function wipe(arr) {
|
||
|
for (var i = 0; i < arr.length; i++)
|
||
|
arr[i] = 0xFE;
|
||
|
}
|
||
|
|
||
|
function nextPowerOfTwo(v) {
|
||
|
var p = 2;
|
||
|
while ((v >>= 1)) {
|
||
|
p <<= 1;
|
||
|
}
|
||
|
return p;
|
||
|
}
|
||
|
|
||
|
function newSession() {
|
||
|
// openssl.js suggests using wasmfs, however, it adds a
|
||
|
// lot of dependencies and is difficult to package, so
|
||
|
// just implement a basic virtual FS here.
|
||
|
|
||
|
let files = { };
|
||
|
let lineBuf = '';
|
||
|
let tty = {
|
||
|
write : function(arr) {
|
||
|
for (var c of arr)
|
||
|
if (c == 10) {
|
||
|
console.log(lineBuf); lineBuf = '';
|
||
|
} else {
|
||
|
lineBuf += String.fromCharCode(c);
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
let fds = [ { }, tty, tty ];
|
||
|
let impl = {
|
||
|
existsSync : function(fn) {
|
||
|
// console.log('existsSync', fn);
|
||
|
return files.hasOwnProperty(fn);
|
||
|
},
|
||
|
realpathSync : function(n) {
|
||
|
return n;
|
||
|
},
|
||
|
mkdirSync : function() {
|
||
|
return false;
|
||
|
},
|
||
|
openSync : function(fn, flags) {
|
||
|
// console.log('openSync', fn, flags);
|
||
|
var ro = flags & 1;
|
||
|
if (!ro)
|
||
|
files[fn] = new Uint8Array(0);
|
||
|
else
|
||
|
if (!files.hasOwnProperty(fn))
|
||
|
return -1;
|
||
|
var pos = 0;
|
||
|
fds.push({
|
||
|
read : function(length) {
|
||
|
var arr = files[fn].subarray(pos, pos + length);
|
||
|
pos += arr.length;
|
||
|
return arr;
|
||
|
},
|
||
|
write : ro ? null : function(arr) {
|
||
|
// console.log('write', arr.length, '@', pos, '/', files[fn].buffer.byteLength);
|
||
|
if (files[fn].buffer.byteLength < pos + arr.length) {
|
||
|
var newSize = nextPowerOfTwo(pos + arr.length);
|
||
|
var newFile = new Uint8Array(newSize);
|
||
|
newFile.set(files[fn]);
|
||
|
wipe(new Uint8Array(files[fn].buffer));
|
||
|
files[fn] = newFile.subarray(0, pos);
|
||
|
}
|
||
|
var target = new Uint8Array(files[fn].buffer, pos, arr.length);
|
||
|
target.set(arr);
|
||
|
pos += arr.length;
|
||
|
files[fn] = new Uint8Array(files[fn].buffer, 0, pos);
|
||
|
wipe(arr);
|
||
|
},
|
||
|
});
|
||
|
return fds.length - 1;
|
||
|
},
|
||
|
readSync : function(fd, buffer, offset, length, position) {
|
||
|
// console.log(fd, buffer, offset, length);
|
||
|
if (fd < 0) return 0;
|
||
|
var arr = fds[fd].read(length);
|
||
|
buffer.subarray(offset, offset + arr.length).set(arr);
|
||
|
return arr.length;
|
||
|
},
|
||
|
writeSync : function(fd, buffer) {
|
||
|
if (fd < 0) return 0;
|
||
|
fds[fd].write(buffer);
|
||
|
return buffer.length;
|
||
|
},
|
||
|
closeSync : function(fd) {
|
||
|
if (fd < 0) return;
|
||
|
fds[fd] = null;
|
||
|
},
|
||
|
fstatSync : function() {
|
||
|
return {
|
||
|
isBlockDevice : () => false,
|
||
|
isCharacterDevice : () => false,
|
||
|
isDirectory : () => false,
|
||
|
isFIFO : () => false,
|
||
|
isFile : () => true,
|
||
|
};
|
||
|
},
|
||
|
constants : {
|
||
|
O_RDONLY : 1,
|
||
|
},
|
||
|
};
|
||
|
|
||
|
let openSSL = new OpenSSL({
|
||
|
fs: impl,
|
||
|
rootDir: '/'
|
||
|
});
|
||
|
|
||
|
return { files, openSSL };
|
||
|
}
|
||
|
|
||
|
async function genSpkac(algorithm, pkeyopts, challenge) {
|
||
|
let s = newSession();
|
||
|
if (algorithm === undefined)
|
||
|
algorithm = 'RSA';
|
||
|
if (!algorithm.length || algorithm.match(/[\s]/))
|
||
|
throw 'Invalid algorithm';
|
||
|
if (challenge !== undefined && (!challenge.length || challenge.match(/[\s]/))) {
|
||
|
// https://github.com/DigitalArsenal/openssl.js/issues/3
|
||
|
throw 'Invalid challenge string';
|
||
|
}
|
||
|
pkeyopts = pkeyopts.split(/[\s]{1,}/g).filter(Boolean);
|
||
|
|
||
|
await s.openSSL.runCommand(
|
||
|
"genpkey" +
|
||
|
" -algorithm " + algorithm +
|
||
|
pkeyopts.map(opt => " -pkeyopt " + opt) +
|
||
|
" -out /private.pem");
|
||
|
if (!s.files.hasOwnProperty('/private.pem') || !s.files['/private.pem'].length)
|
||
|
throw 'Private key generation failed';
|
||
|
|
||
|
await s.openSSL.runCommand(
|
||
|
"spkac" +
|
||
|
" -key /private.pem" +
|
||
|
" -out /spkac" +
|
||
|
(challenge === undefined ? "" : " -challenge " + challenge)
|
||
|
);
|
||
|
if (!s.files.hasOwnProperty('/spkac') || !s.files['/spkac'].length)
|
||
|
throw 'Private key generation failed';
|
||
|
var privateKey = s.files['/private.pem'];
|
||
|
|
||
|
var spkac = s.files['/spkac'];
|
||
|
spkac = spkac.subarray(6, spkac.length - 1); // Skip "SKPAC=" and trim line ending
|
||
|
spkac = String.fromCharCode.apply(null, spkac);
|
||
|
return { spkac, privateKey };
|
||
|
}
|
||
|
|
||
|
async function convertDerToPem(der) {
|
||
|
let s = newSession();
|
||
|
s.files['/cert.der'] = der;
|
||
|
await s.openSSL.runCommand(
|
||
|
"x509" +
|
||
|
" -inform der" +
|
||
|
" -in /cert.der" +
|
||
|
" -outform pem" +
|
||
|
" -out /cert.pem");
|
||
|
if (!s.files.hasOwnProperty('/cert.pem') || !s.files['/cert.pem'].length)
|
||
|
throw 'Certificate conversion failed';
|
||
|
return s.files['/cert.pem'];
|
||
|
}
|
||
|
|
||
|
async function convertToPkcs12(key, pem) {
|
||
|
let s = newSession();
|
||
|
s.files['/private.pem'] = key;
|
||
|
s.files['/cert.pem'] = pem;
|
||
|
await s.openSSL.runCommand(
|
||
|
"pkcs12" +
|
||
|
" -export" +
|
||
|
" -in /cert.pem" +
|
||
|
" -inkey /private.pem" +
|
||
|
" -out /cert.p12" +
|
||
|
" -passout pass:mac");
|
||
|
if (!s.files.hasOwnProperty('/cert.p12') || !s.files['/cert.p12'].length)
|
||
|
throw 'Certificate conversion failed';
|
||
|
return s.files['/cert.p12'];
|
||
|
}
|
||
|
|
||
|
async function base64Encode(data) {
|
||
|
let s = newSession();
|
||
|
s.files['/data.bin'] = data;
|
||
|
await s.openSSL.runCommand(
|
||
|
"base64" +
|
||
|
" -e" +
|
||
|
" -in /data.bin" +
|
||
|
" -out /data.txt");
|
||
|
if (!s.files.hasOwnProperty('/data.txt'))
|
||
|
throw 'Base64 encoding failed';
|
||
|
return s.files['/data.txt'];
|
||
|
}
|
||
|
|
||
|
function polyfillKeygen(keygen) {
|
||
|
var name = keygen.getAttribute('name');
|
||
|
if (!name) throw '<keygen> element has no name';
|
||
|
|
||
|
var autofocus = keygen.hasAttribute('autofocus');
|
||
|
if (autofocus)
|
||
|
keygen.removeAttribute('autofocus');
|
||
|
|
||
|
var disabled = keygen.hasAttribute('disabled');
|
||
|
|
||
|
if (keygen.hasAttribute('keyparams')) // Don't do the wrong thing
|
||
|
throw 'Sorry, the keyparams <keygen> attribute is not supported';
|
||
|
|
||
|
var algoSelect;
|
||
|
if (!keygen.hasAttribute('keytype')) {
|
||
|
algoSelect = document.createElement('select');
|
||
|
algoSelect.setAttribute('title', 'Private key algorithm');
|
||
|
algoSelect.setAttribute('class', 'keygen-algorithm');
|
||
|
for (var algoName of supportedAlgorithms) {
|
||
|
var algoOption = document.createElement('option');
|
||
|
algoOption.setAttribute('value', algoName);
|
||
|
algoOption.textContent = algoName;
|
||
|
algoSelect.appendChild(algoOption);
|
||
|
}
|
||
|
if (autofocus) {
|
||
|
algoSelect.setAttribute('autofocus', '');
|
||
|
autofocus = false;
|
||
|
}
|
||
|
if (disabled)
|
||
|
algoSelect.setAttribute('disabled', '');
|
||
|
keygen.appendChild(algoSelect);
|
||
|
}
|
||
|
|
||
|
var optInput = document.createElement('input');
|
||
|
optInput.setAttribute('title', 'Space-separated public key generation algorithm options\n' +
|
||
|
'(e.g. "rsa_keygen_bits:4096" for a 4096-bit RSA key)');
|
||
|
optInput.setAttribute('class', 'keygen-pkeyopts');
|
||
|
optInput.setAttribute('placeholder', 'optional private key options');
|
||
|
if (autofocus) {
|
||
|
optInput.setAttribute('autofocus', '');
|
||
|
autofocus = false;
|
||
|
}
|
||
|
if (disabled)
|
||
|
optInput.setAttribute('disabled', '');
|
||
|
keygen.appendChild(optInput);
|
||
|
|
||
|
var status = document.createElement('span');
|
||
|
status.setAttribute('class', 'keygen-status');
|
||
|
status.textContent = 'Ready!';
|
||
|
keygen.appendChild(status);
|
||
|
|
||
|
var resultLink = document.createElement('a');
|
||
|
resultLink.setAttribute('class', 'keygen-link-inv');
|
||
|
keygen.appendChild(resultLink);
|
||
|
|
||
|
var form;
|
||
|
if (keygen.hasAttribute('form')) {
|
||
|
form = document.getElementById(keygen.getAttribute('form'));
|
||
|
if (!form) throw '<keygen> has invalid form attribute';
|
||
|
}
|
||
|
else {
|
||
|
form = optInput.form;
|
||
|
if (!form) throw '<keygen> is not in a <form> and has no form attribute';
|
||
|
}
|
||
|
|
||
|
var spkacInput = document.createElement('input');
|
||
|
spkacInput.setAttribute('type', 'hidden');
|
||
|
keygen.removeAttribute('name');
|
||
|
spkacInput.setAttribute('name', name);
|
||
|
if (disabled)
|
||
|
spkacInput.setAttribute('disabled', '');
|
||
|
form.appendChild(spkacInput);
|
||
|
|
||
|
form.addEventListener('submit', function(e) {
|
||
|
e.preventDefault();
|
||
|
|
||
|
(async function() {
|
||
|
var algorithm = keygen.hasAttribute('keytype')
|
||
|
? keygen.getAttribute('keytype')
|
||
|
: algoSelect.value;
|
||
|
var challenge;
|
||
|
if (keygen.hasAttribute('challenge'))
|
||
|
challenge = keygen.getAttribute('challenge');
|
||
|
var pkeyopts = optInput.value;
|
||
|
if (keygen.hasAttribute('-keygenjs-pkeyopts'))
|
||
|
pkeyopts += ' ' + keygen.getAttribute('-keygenjs-pkeyopts');
|
||
|
|
||
|
resultLink.setAttribute('class', 'keygen-link-inv');
|
||
|
resultLink.setAttribute('href', '');
|
||
|
resultLink.textContent = '';
|
||
|
|
||
|
var spkac;
|
||
|
status.textContent = 'Generating key pair...';
|
||
|
try {
|
||
|
spkac = await genSpkac(algorithm, pkeyopts, challenge);
|
||
|
} catch (e) {
|
||
|
console.error('keygen.js key generation error:', e);
|
||
|
status.textContent = 'Error!';
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
spkacInput.setAttribute('value', spkac.spkac);
|
||
|
status.textContent = 'Submitting SPKAC request...';
|
||
|
var data = new FormData(form);
|
||
|
var xhr = new XMLHttpRequest();
|
||
|
xhr.open(form.method || 'GET', form.action || '', true);
|
||
|
xhr.responseType = 'arraybuffer';
|
||
|
xhr.onload = function() {
|
||
|
status.textContent = 'Processing response...';
|
||
|
try {
|
||
|
if (!xhr.response) throw 'No response';
|
||
|
var der = new Uint8Array(xhr.response);
|
||
|
if (!der.length) throw 'Empty response';
|
||
|
|
||
|
(async function() {
|
||
|
var pem;
|
||
|
status.textContent = 'Converting certificate...';
|
||
|
try {
|
||
|
pem = await convertDerToPem(der);
|
||
|
} catch (e) {
|
||
|
console.error('keygen.js certificate conversion error:', e);
|
||
|
status.textContent = 'Error!';
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
var p12;
|
||
|
status.textContent = 'Combining certificate...';
|
||
|
try {
|
||
|
p12 = await convertToPkcs12(spkac.privateKey, pem);
|
||
|
} catch (e) {
|
||
|
console.error('keygen.js certificate combining error:', e);
|
||
|
status.textContent = 'Error!';
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
var b64 = await base64Encode(p12);
|
||
|
var url = 'data:application/x-pkcs12;base64,' +
|
||
|
String.fromCharCode.apply(null, b64);
|
||
|
|
||
|
status.textContent = ''; // OK
|
||
|
resultLink.setAttribute('class', 'keygen-link');
|
||
|
resultLink.setAttribute('href', url);
|
||
|
resultLink.setAttribute('download', 'cert.p12');
|
||
|
resultLink.setAttribute('onclick', 'myagent.play("Save");');
|
||
|
resultLink.textContent = 'Save certificate';
|
||
|
})();
|
||
|
} catch (e) {
|
||
|
console.error('keygen.js certificate generation error:', e);
|
||
|
status.textContent = 'Error!';
|
||
|
}
|
||
|
};
|
||
|
xhr.send(data);
|
||
|
|
||
|
})();
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function polyfillAll() {
|
||
|
var keygens = document.getElementsByTagName('keygen');
|
||
|
for (var keygen of keygens) {
|
||
|
try {
|
||
|
polyfillKeygen(keygen);
|
||
|
} catch (e) {
|
||
|
console.error('keygen.js polyfill error:', e);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (document.readyState === 'loading')
|
||
|
document.addEventListener('DOMContentLoaded', polyfillAll);
|
||
|
else
|
||
|
polyfillAll();
|
||
|
}
|