mirror of
https://github.com/darkwire/darkwire.io.git
synced 2025-07-24 05:00:17 +00:00
Use HMAC symmetric key to sign/verify and add webkit shim
For webkit support, I removed the public/private signing keys and replaced them with a symmetric HMAC signing key. It achieves the same thing and besides adding webkit support, is also a bit cleaner. Webkit handles key export is a non-standard way, so with this shim we have to export and import public keys in “spki” format. Webkit also requires slightly different options to be passed in for some operations.
This commit is contained in:
parent
663fee0797
commit
0dad45ed91
325
src/js/main.js
325
src/js/main.js
@ -61,9 +61,6 @@ $(function() {
|
||||
this.setSelectionRange(0, 9999);
|
||||
});
|
||||
|
||||
var crypto = window.crypto;
|
||||
var cryptoSubtle = window.crypto.subtle || window.crypto.webkitSubtle;
|
||||
|
||||
let socket = io(roomId);
|
||||
|
||||
FastClick.attach(document.body);
|
||||
@ -93,107 +90,22 @@ $(function() {
|
||||
$inputMessage.focus();
|
||||
|
||||
Promise.all([
|
||||
createPrimaryKeys(),
|
||||
createSigningKeys()
|
||||
createPrimaryKeys();
|
||||
])
|
||||
.then(function(data) {
|
||||
keys.primary = {
|
||||
keys = {
|
||||
public: data[0].publicKey,
|
||||
private: data[0].privateKey
|
||||
};
|
||||
keys.signing = {
|
||||
public: data[1].publicKey,
|
||||
private: data[1].privateKey
|
||||
};
|
||||
return Promise.all([
|
||||
exportKey(data[0].publicKey),
|
||||
exportKey(data[1].publicKey)
|
||||
exportKey(data[0].publicKey, "spki")
|
||||
]);
|
||||
})
|
||||
.then(function(exportedKeys) {
|
||||
// Tell the server your username and send public keys
|
||||
socket.emit('add user', {
|
||||
username: username,
|
||||
publicPrimaryKey: exportedKeys[0],
|
||||
publicSigningKey: exportedKeys[1]
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Sends a chat message
|
||||
function sendMessage () {
|
||||
// Don't send unless other users exist
|
||||
if (users.length <= 1) return;
|
||||
|
||||
let message = $inputMessage.val();
|
||||
// Prevent markup from being injected into the message
|
||||
message = cleanInput(message);
|
||||
// if there is a non-empty message and a socket connection
|
||||
if (message && connected) {
|
||||
$inputMessage.val('');
|
||||
$('#send-message-btn').removeClass('active');
|
||||
addChatMessage({
|
||||
username: username,
|
||||
message: message
|
||||
});
|
||||
let vector = crypto.getRandomValues(new Uint8Array(16));
|
||||
|
||||
let secretKey;
|
||||
let secretKeys;
|
||||
let messageData;
|
||||
let signature;
|
||||
|
||||
// Generate new secret key and vector for each message
|
||||
createSecretKey()
|
||||
.then(function(key) {
|
||||
secretKey = key;
|
||||
// Generate secretKey and encrypt with each user's public key
|
||||
let promises = [];
|
||||
_.each(users, function(user) {
|
||||
// It not me
|
||||
if (user.username !== window.username) {
|
||||
let promise = new Promise(function(resolve, reject) {
|
||||
let thisUser = user;
|
||||
|
||||
let exportedSecretKey;
|
||||
exportKey(key, "raw")
|
||||
.then(function(data) {
|
||||
exportedSecretKey = data;
|
||||
return encryptSecretKey(data, thisUser.publicPrimaryKey);
|
||||
})
|
||||
.then(function(encryptedSecretKey) {
|
||||
var encData = new Uint8Array(encryptedSecretKey);
|
||||
var str = convertArrayBufferViewToString(encData);
|
||||
resolve({
|
||||
id: thisUser.id,
|
||||
secretKey: str
|
||||
});
|
||||
});
|
||||
});
|
||||
promises.push(promise);
|
||||
}
|
||||
});
|
||||
return Promise.all(promises);
|
||||
})
|
||||
.then(function(data) {
|
||||
secretKeys = data;
|
||||
messageData = convertStringToArrayBufferView(message);
|
||||
return signKey(messageData, keys.signing.private)
|
||||
})
|
||||
.then(function(data) {
|
||||
signature = data;
|
||||
return encryptMessage(messageData, secretKey, vector)
|
||||
})
|
||||
.then(function(encryptedData) {
|
||||
let msg = convertArrayBufferViewToString(new Uint8Array(encryptedData));
|
||||
let vct = convertArrayBufferViewToString(new Uint8Array(vector));
|
||||
let sig = convertArrayBufferViewToString(new Uint8Array(signature));
|
||||
socket.emit('new message', {
|
||||
message: msg,
|
||||
vector: vct,
|
||||
secretKeys: secretKeys,
|
||||
signature: sig
|
||||
publicKey: exportedKeys[0]
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -387,15 +299,13 @@ $(function() {
|
||||
let promise = new Promise(function(resolve, reject) {
|
||||
let currentUser = user;
|
||||
Promise.all([
|
||||
importPrimaryKey(currentUser.publicPrimaryKey),
|
||||
importSigningKey(currentUser.publicSigningKey)
|
||||
importPrimaryKey(currentUser.publicKey, "spki")
|
||||
])
|
||||
.then(function(keys) {
|
||||
users.push({
|
||||
id: currentUser.id,
|
||||
username: currentUser.username,
|
||||
publicPrimaryKey: keys[0],
|
||||
publicSigningKey: keys[1]
|
||||
publicKey: keys[0]
|
||||
});
|
||||
resolve();
|
||||
});
|
||||
@ -417,10 +327,6 @@ $(function() {
|
||||
$('#first-modal').modal('show');
|
||||
}
|
||||
|
||||
$('.modal').on('shown.bs.modal', function (e) {
|
||||
autosize.update($('textarea.share-text'));
|
||||
});
|
||||
|
||||
log(data.username + ' joined');
|
||||
|
||||
renderParticipantsList();
|
||||
@ -428,6 +334,103 @@ $(function() {
|
||||
|
||||
});
|
||||
|
||||
// Sends a chat message
|
||||
function sendMessage () {
|
||||
// Don't send unless other users exist
|
||||
if (users.length <= 1) return;
|
||||
|
||||
let message = $inputMessage.val();
|
||||
// Prevent markup from being injected into the message
|
||||
message = cleanInput(message);
|
||||
// if there is a non-empty message and a socket connection
|
||||
if (message && connected) {
|
||||
$inputMessage.val('');
|
||||
$('#send-message-btn').removeClass('active');
|
||||
addChatMessage({
|
||||
username: username,
|
||||
message: message
|
||||
});
|
||||
let vector = crypto.getRandomValues(new Uint8Array(16));
|
||||
|
||||
let secretKey;
|
||||
let secretKeys;
|
||||
let messageData;
|
||||
let signature;
|
||||
let signingKey;
|
||||
let encryptedMessageData;
|
||||
|
||||
// Generate new secret key and vector for each message
|
||||
createSecretKey()
|
||||
.then(function(key) {
|
||||
secretKey = key;
|
||||
return createSigningKey();
|
||||
})
|
||||
.then(function(key) {
|
||||
signingKey = key;
|
||||
// Generate secretKey and encrypt with each user's public key
|
||||
let promises = [];
|
||||
_.each(users, function(user) {
|
||||
// If not me
|
||||
if (user.username !== window.username) {
|
||||
let promise = new Promise(function(resolve, reject) {
|
||||
let thisUser = user;
|
||||
|
||||
let secretKeyStr;
|
||||
|
||||
// Export secret key
|
||||
exportKey(secretKey, "raw")
|
||||
.then(function(data) {
|
||||
return encryptSecretKey(data, thisUser.publicKey);
|
||||
})
|
||||
.then(function(encryptedSecretKey) {
|
||||
let encData = new Uint8Array(encryptedSecretKey);
|
||||
secretKeyStr = convertArrayBufferViewToString(encData);
|
||||
// Export HMAC signing key
|
||||
return exportKey(signingKey, "raw");
|
||||
})
|
||||
.then(function(data) {
|
||||
// Encrypt signing key with user's public key
|
||||
return encryptSigningKey(data, thisUser.publicKey);
|
||||
})
|
||||
.then(function(encryptedSigningKey) {
|
||||
let encData = new Uint8Array(encryptedSigningKey);
|
||||
var str = convertArrayBufferViewToString(encData);
|
||||
resolve({
|
||||
id: thisUser.id,
|
||||
secretKey: secretKeyStr,
|
||||
encryptedSigningKey: str
|
||||
});
|
||||
});
|
||||
});
|
||||
promises.push(promise);
|
||||
}
|
||||
});
|
||||
return Promise.all(promises);
|
||||
})
|
||||
.then(function(data) {
|
||||
secretKeys = data;
|
||||
messageData = convertStringToArrayBufferView(message);
|
||||
return signKey(messageData, signingKey);
|
||||
})
|
||||
.then(function(data) {
|
||||
signature = data;
|
||||
return encryptMessage(messageData, secretKey, vector);
|
||||
})
|
||||
.then(function(data) {
|
||||
encryptedMessageData = data;
|
||||
let msg = convertArrayBufferViewToString(new Uint8Array(encryptedMessageData));
|
||||
let vct = convertArrayBufferViewToString(new Uint8Array(vector));
|
||||
let sig = convertArrayBufferViewToString(new Uint8Array(signature));
|
||||
socket.emit('new message', {
|
||||
message: msg,
|
||||
vector: vct,
|
||||
secretKeys: secretKeys,
|
||||
signature: sig
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Whenever the server emits 'new message', update the chat body
|
||||
socket.on('new message', function (data) {
|
||||
// Don't show messages if no key
|
||||
@ -447,7 +450,7 @@ $(function() {
|
||||
let vectorData = convertStringToArrayBufferView(vector);
|
||||
let secretKeys = data.secretKeys;
|
||||
let decryptedMessageData;
|
||||
let decryptedMessage;
|
||||
let decryptedMessage;
|
||||
|
||||
let mySecretKey = _.find(secretKeys, function(key) {
|
||||
return key.id === myUserId;
|
||||
@ -455,13 +458,11 @@ $(function() {
|
||||
let signature = data.signature;
|
||||
let signatureData = convertStringToArrayBufferView(signature);
|
||||
let secretKeyArrayBuffer = convertStringToArrayBufferView(mySecretKey.secretKey);
|
||||
let signingKeyArrayBuffer = convertStringToArrayBufferView(mySecretKey.encryptedSigningKey);
|
||||
|
||||
decryptSecretKey(secretKeyArrayBuffer, keys.primary.private)
|
||||
decryptSecretKey(secretKeyArrayBuffer, keys.private)
|
||||
.then(function(data) {
|
||||
return new Uint8Array(data);
|
||||
})
|
||||
.then(function(data) {
|
||||
return importSecretKey(data, "raw");
|
||||
return importSecretKey(new Uint8Array(data), "raw");
|
||||
})
|
||||
.then(function(data) {
|
||||
let secretKey = data;
|
||||
@ -470,14 +471,14 @@ $(function() {
|
||||
.then(function(data) {
|
||||
decryptedMessageData = data;
|
||||
decryptedMessage = convertArrayBufferViewToString(new Uint8Array(data))
|
||||
return decryptSigningKey(signingKeyArrayBuffer, keys.private)
|
||||
})
|
||||
.then(function() {
|
||||
// Find who sent msg (senderId), get their public key and verifyKey() with it and signature
|
||||
let sender = _.find(users, function(user) {
|
||||
return user.id === senderId;
|
||||
});
|
||||
let senderPublicVerifyKey = sender.publicSigningKey;
|
||||
return verifyKey(signatureData, decryptedMessageData, senderPublicVerifyKey)
|
||||
.then(function(data) {
|
||||
return importSigningKey(new Uint8Array(data), "raw");
|
||||
})
|
||||
.then(function(data) {
|
||||
let signingKey = data;
|
||||
return verifyKey(signatureData, decryptedMessageData, signingKey);
|
||||
})
|
||||
.then(function(bool) {
|
||||
if (bool) {
|
||||
@ -587,13 +588,12 @@ $(function() {
|
||||
return str;
|
||||
}
|
||||
|
||||
function createSigningKeys() {
|
||||
return cryptoSubtle.generateKey(
|
||||
function createSigningKey() {
|
||||
return window.crypto.subtle.generateKey(
|
||||
{
|
||||
name: "RSASSA-PKCS1-v1_5",
|
||||
modulusLength: 2048, //can be 1024, 2048, or 4096
|
||||
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
|
||||
name: "HMAC",
|
||||
hash: {name: "SHA-256"}, //can be "SHA-1", "SHA-256", "SHA-384", or "SHA-512"
|
||||
//length: 256, //optional, if you want your key length to differ from the hash function's block length
|
||||
},
|
||||
true, //whether the key is extractable (i.e. can be used in exportKey)
|
||||
["sign", "verify"] //can be any combination of "sign" and "verify"
|
||||
@ -601,7 +601,7 @@ $(function() {
|
||||
}
|
||||
|
||||
function createPrimaryKeys() {
|
||||
return cryptoSubtle.generateKey(
|
||||
return window.crypto.subtle.generateKey(
|
||||
{
|
||||
name: "RSA-OAEP",
|
||||
modulusLength: 2048, //can be 1024, 2048, or 4096
|
||||
@ -614,21 +614,24 @@ $(function() {
|
||||
}
|
||||
|
||||
function createSecretKey() {
|
||||
return cryptoSubtle.generateKey(
|
||||
return window.crypto.subtle.generateKey(
|
||||
{
|
||||
name: "AES-CBC",
|
||||
length: 256, //can be 128, 192, or 256
|
||||
},
|
||||
true, //whether the key is extractable (i.e. can be used in exportKey)
|
||||
["encrypt", "decrypt", "wrapKey", "unwrapKey"] //can be "encrypt", "decrypt", "wrapKey", or "unwrapKey"
|
||||
["encrypt", "decrypt"] //can be "encrypt", "decrypt", "wrapKey", or "unwrapKey"
|
||||
);
|
||||
}
|
||||
|
||||
function encryptSecretKey(data, secretKey) {
|
||||
// Secret key will be recipient's public key
|
||||
return cryptoSubtle.encrypt(
|
||||
return window.crypto.subtle.encrypt(
|
||||
{
|
||||
name: "RSA-OAEP"
|
||||
name: "RSA-OAEP",
|
||||
modulusLength: 2048,
|
||||
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
|
||||
hash: {name: "SHA-256"}
|
||||
},
|
||||
secretKey,
|
||||
data //ArrayBuffer of data you want to encrypt
|
||||
@ -637,9 +640,41 @@ $(function() {
|
||||
|
||||
function decryptSecretKey(data, key) {
|
||||
// key will be my private key
|
||||
return cryptoSubtle.decrypt(
|
||||
return window.crypto.subtle.decrypt(
|
||||
{
|
||||
name: "RSA-OAEP",
|
||||
modulusLength: 2048,
|
||||
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
|
||||
hash: {name: "SHA-256"}
|
||||
//label: Uint8Array([...]) //optional
|
||||
},
|
||||
key,
|
||||
data //ArrayBuffer of the data
|
||||
);
|
||||
}
|
||||
|
||||
function encryptSigningKey(data, signingKey) {
|
||||
// Secret key will be recipient's public key
|
||||
return window.crypto.subtle.encrypt(
|
||||
{
|
||||
name: "RSA-OAEP",
|
||||
modulusLength: 2048,
|
||||
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
|
||||
hash: {name: "SHA-256"}
|
||||
},
|
||||
signingKey,
|
||||
data //ArrayBuffer of data you want to encrypt
|
||||
);
|
||||
}
|
||||
|
||||
function decryptSigningKey(data, key) {
|
||||
// key will be my private key
|
||||
return window.crypto.subtle.decrypt(
|
||||
{
|
||||
name: "RSA-OAEP",
|
||||
modulusLength: 2048,
|
||||
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
|
||||
hash: {name: "SHA-256"}
|
||||
//label: Uint8Array([...]) //optional
|
||||
},
|
||||
key,
|
||||
@ -648,7 +683,7 @@ $(function() {
|
||||
}
|
||||
|
||||
function encryptMessage(data, secretKey, iv) {
|
||||
return cryptoSubtle.encrypt(
|
||||
return window.crypto.subtle.encrypt(
|
||||
{
|
||||
name: "AES-CBC",
|
||||
//Don't re-use initialization vectors!
|
||||
@ -661,7 +696,7 @@ $(function() {
|
||||
}
|
||||
|
||||
function decryptMessage(data, secretKey, iv) {
|
||||
return cryptoSubtle.decrypt(
|
||||
return window.crypto.subtle.decrypt(
|
||||
{
|
||||
name: "AES-CBC",
|
||||
iv: iv, //The initialization vector you used to encrypt
|
||||
@ -672,7 +707,7 @@ $(function() {
|
||||
}
|
||||
|
||||
function importSecretKey(jwkData, format) {
|
||||
return cryptoSubtle.importKey(
|
||||
return window.crypto.subtle.importKey(
|
||||
format || "jwk", //can be "jwk" or "raw"
|
||||
//this is an example jwk key, "raw" would be an ArrayBuffer
|
||||
jwkData,
|
||||
@ -684,15 +719,18 @@ $(function() {
|
||||
);
|
||||
}
|
||||
|
||||
function importPrimaryKey(jwkData) {
|
||||
function importPrimaryKey(jwkData, format) {
|
||||
// Will be someone's public key
|
||||
return cryptoSubtle.importKey(
|
||||
"jwk", //can be "jwk" (public or private), "spki" (public only), or "pkcs8" (private only)
|
||||
let hashObj = {
|
||||
name: "RSA-OAEP"
|
||||
};
|
||||
if (!window.crypto.webkitSubtle) {
|
||||
hashObj.hash = {name: "SHA-256"};
|
||||
}
|
||||
return window.crypto.subtle.importKey(
|
||||
format || "jwk", //can be "jwk" (public or private), "spki" (public only), or "pkcs8" (private only)
|
||||
jwkData,
|
||||
{ //these are the algorithm options
|
||||
name: "RSA-OAEP",
|
||||
hash: {name: "SHA-256"}, //can be "SHA-1", "SHA-256", "SHA-384", or "SHA-512"
|
||||
},
|
||||
hashObj,
|
||||
true, //whether the key is extractable (i.e. can be used in exportKey)
|
||||
["encrypt"] //"encrypt" or "wrapKey" for public key import or
|
||||
//"decrypt" or "unwrapKey" for private key imports
|
||||
@ -701,20 +739,21 @@ $(function() {
|
||||
|
||||
function exportKey(key, format) {
|
||||
// Will be public primary key or public signing key
|
||||
return cryptoSubtle.exportKey(
|
||||
return window.crypto.subtle.exportKey(
|
||||
format || "jwk", //can be "jwk" (public or private), "spki" (public only), or "pkcs8" (private only)
|
||||
key //can be a publicKey or privateKey, as long as extractable was true
|
||||
);
|
||||
}
|
||||
|
||||
function importSigningKey(jwkData) {
|
||||
return cryptoSubtle.importKey(
|
||||
"jwk", //can be "jwk" (public or private), "spki" (public only), or "pkcs8" (private only)
|
||||
return window.crypto.subtle.importKey(
|
||||
"raw", //can be "jwk" (public or private), "spki" (public only), or "pkcs8" (private only)
|
||||
//this is an example jwk key, other key types are Uint8Array objects
|
||||
jwkData,
|
||||
{ //these are the algorithm options
|
||||
name: "RSASSA-PKCS1-v1_5",
|
||||
name: "HMAC",
|
||||
hash: {name: "SHA-256"}, //can be "SHA-1", "SHA-256", "SHA-384", or "SHA-512"
|
||||
//length: 256, //optional, if you want your key length to differ from the hash function's block length
|
||||
},
|
||||
true, //whether the key is extractable (i.e. can be used in exportKey)
|
||||
["verify"] //"verify" for public key import, "sign" for private key imports
|
||||
@ -723,9 +762,10 @@ $(function() {
|
||||
|
||||
function signKey(data, keyToSignWith) {
|
||||
// Will use my private key
|
||||
return cryptoSubtle.sign(
|
||||
return window.crypto.subtle.sign(
|
||||
{
|
||||
name: "RSASSA-PKCS1-v1_5"
|
||||
name: "HMAC",
|
||||
hash: {name: "SHA-256"}
|
||||
},
|
||||
keyToSignWith, //from generateKey or importKey above
|
||||
data //ArrayBuffer of data you want to sign
|
||||
@ -734,9 +774,10 @@ $(function() {
|
||||
|
||||
function verifyKey(signature, data, keyToVerifyWith) {
|
||||
// Will verify with sender's public key
|
||||
return cryptoSubtle.verify(
|
||||
return window.crypto.subtle.verify(
|
||||
{
|
||||
name: "RSASSA-PKCS1-v1_5"
|
||||
name: "HMAC",
|
||||
hash: {name: "SHA-256"}
|
||||
},
|
||||
keyToVerifyWith, //from generateKey or importKey above
|
||||
signature, //ArrayBuffer of the signature
|
||||
|
597
src/public/vendor/web-crypto-shim.js
vendored
Normal file
597
src/public/vendor/web-crypto-shim.js
vendored
Normal file
@ -0,0 +1,597 @@
|
||||
/**
|
||||
* @file Web Cryptography API shim
|
||||
* @author Artem S Vybornov <vybornov@gmail.com>
|
||||
* @license MIT
|
||||
*/
|
||||
!function ( global ) {
|
||||
'use strict';
|
||||
|
||||
if ( typeof Promise !== 'function' )
|
||||
throw "Promise support required";
|
||||
|
||||
var _crypto = global.crypto || global.msCrypto;
|
||||
if ( !_crypto ) return;
|
||||
|
||||
var _subtle = _crypto.subtle || _crypto.webkitSubtle;
|
||||
if ( !_subtle ) return;
|
||||
|
||||
var _Crypto = global.Crypto || _crypto.constructor || Object,
|
||||
_SubtleCrypto = global.SubtleCrypto || _subtle.constructor || Object,
|
||||
_CryptoKey = global.CryptoKey || global.Key || Object;
|
||||
|
||||
var isIE = !!global.msCrypto,
|
||||
isWebkit = !!_crypto.webkitSubtle;
|
||||
if ( !isIE && !isWebkit ) return;
|
||||
|
||||
function s2a ( s ) {
|
||||
return btoa(s).replace(/\=+$/, '').replace(/\+/g, '-').replace(/\//g, '_');
|
||||
}
|
||||
|
||||
function a2s ( s ) {
|
||||
s += '===', s = s.slice( 0, -s.length % 4 );
|
||||
return atob( s.replace(/-/g, '+').replace(/_/g, '/') );
|
||||
}
|
||||
|
||||
function s2b ( s ) {
|
||||
var b = new Uint8Array(s.length);
|
||||
for ( var i = 0; i < s.length; i++ ) b[i] = s.charCodeAt(i);
|
||||
return b;
|
||||
}
|
||||
|
||||
function b2s ( b ) {
|
||||
if ( b instanceof ArrayBuffer ) b = new Uint8Array(b);
|
||||
return String.fromCharCode.apply( String, b );
|
||||
}
|
||||
|
||||
function alg ( a ) {
|
||||
var r = { 'name': (a.name || a || '').toUpperCase().replace('V','v') };
|
||||
switch ( r.name ) {
|
||||
case 'SHA-1':
|
||||
case 'SHA-256':
|
||||
case 'SHA-384':
|
||||
case 'SHA-512':
|
||||
break;
|
||||
case 'AES-CBC':
|
||||
case 'AES-GCM':
|
||||
case 'AES-KW':
|
||||
if ( a.length ) r['length'] = a.length;
|
||||
break;
|
||||
case 'HMAC':
|
||||
if ( a.hash ) r['hash'] = alg(a.hash);
|
||||
if ( a.length ) r['length'] = a.length;
|
||||
break;
|
||||
case 'RSAES-PKCS1-v1_5':
|
||||
if ( a.publicExponent ) r['publicExponent'] = new Uint8Array(a.publicExponent);
|
||||
if ( a.modulusLength ) r['modulusLength'] = a.modulusLength;
|
||||
break;
|
||||
case 'RSASSA-PKCS1-v1_5':
|
||||
case 'RSA-OAEP':
|
||||
if ( a.hash ) r['hash'] = alg(a.hash);
|
||||
if ( a.publicExponent ) r['publicExponent'] = new Uint8Array(a.publicExponent);
|
||||
if ( a.modulusLength ) r['modulusLength'] = a.modulusLength;
|
||||
break;
|
||||
default:
|
||||
throw new SyntaxError("Bad algorithm name");
|
||||
}
|
||||
return r;
|
||||
};
|
||||
|
||||
function jwkAlg ( a ) {
|
||||
return {
|
||||
'HMAC': {
|
||||
'SHA-1': 'HS1',
|
||||
'SHA-256': 'HS256',
|
||||
'SHA-384': 'HS384',
|
||||
'SHA-512': 'HS512',
|
||||
},
|
||||
'RSASSA-PKCS1-v1_5': {
|
||||
'SHA-1': 'RS1',
|
||||
'SHA-256': 'RS256',
|
||||
'SHA-384': 'RS384',
|
||||
'SHA-512': 'RS512',
|
||||
},
|
||||
'RSAES-PKCS1-v1_5': {
|
||||
'': 'RSA1_5',
|
||||
},
|
||||
'RSA-OAEP': {
|
||||
'SHA-1': 'RSA-OAEP',
|
||||
'SHA-256': 'RSA-OAEP-256',
|
||||
},
|
||||
'AES-KW': {
|
||||
'128': 'A128KW',
|
||||
'192': 'A192KW',
|
||||
'256': 'A256KW',
|
||||
},
|
||||
'AES-GCM': {
|
||||
'128': 'A128GCM',
|
||||
'192': 'A192GCM',
|
||||
'256': 'A256GCM',
|
||||
},
|
||||
'AES-CBC': {
|
||||
'128': 'A128CBC',
|
||||
'192': 'A192CBC',
|
||||
'256': 'A256CBC',
|
||||
},
|
||||
}[a.name][ ( a.hash || {} ).name || a.length || '' ];
|
||||
}
|
||||
|
||||
function b2jwk ( k ) {
|
||||
if ( k instanceof ArrayBuffer || k instanceof Uint8Array ) k = JSON.parse( decodeURIComponent( escape( b2s(k) ) ) );
|
||||
var jwk = { 'kty': k.kty, 'alg': k.alg, 'ext': k.ext || k.extractable };
|
||||
switch ( jwk.kty ) {
|
||||
case 'oct':
|
||||
jwk.k = k.k;
|
||||
case 'RSA':
|
||||
[ 'n', 'e', 'd', 'p', 'q', 'dp', 'dq', 'qi', 'oth' ].forEach( function ( x ) { if ( x in k ) jwk[x] = k[x] } );
|
||||
break;
|
||||
default:
|
||||
throw new TypeError("Unsupported key type");
|
||||
}
|
||||
return jwk;
|
||||
}
|
||||
|
||||
function jwk2b ( k ) {
|
||||
var jwk = b2jwk(k);
|
||||
if ( isIE ) jwk['extractable'] = jwk.ext, delete jwk.ext;
|
||||
return s2b( unescape( encodeURIComponent( JSON.stringify(jwk) ) ) ).buffer;
|
||||
}
|
||||
|
||||
function pkcs2jwk ( k ) {
|
||||
var info = b2der(k), prv = false;
|
||||
if ( info.length > 2 ) prv = true, info.shift(); // remove version from PKCS#8 PrivateKeyInfo structure
|
||||
var jwk = { 'ext': true };
|
||||
switch ( info[0][0] ) {
|
||||
case '1.2.840.113549.1.1.1':
|
||||
var rsaComp = [ 'n', 'e', 'd', 'p', 'q', 'dp', 'dq', 'qi' ],
|
||||
rsaKey = b2der( info[1] );
|
||||
if ( prv ) rsaKey.shift(); // remove version from PKCS#1 RSAPrivateKey structure
|
||||
for ( var i = 0; i < rsaKey.length; i++ ) {
|
||||
if ( !rsaKey[i][0] ) rsaKey[i] = rsaKey[i].subarray(1);
|
||||
jwk[ rsaComp[i] ] = s2a( b2s( rsaKey[i] ) );
|
||||
}
|
||||
jwk['kty'] = 'RSA';
|
||||
break;
|
||||
default:
|
||||
throw new TypeError("Unsupported key type");
|
||||
}
|
||||
return jwk;
|
||||
}
|
||||
|
||||
function jwk2pkcs ( k ) {
|
||||
var key, info = [ [ '', null ] ], prv = false;
|
||||
switch ( k.kty ) {
|
||||
case 'RSA':
|
||||
var rsaComp = [ 'n', 'e', 'd', 'p', 'q', 'dp', 'dq', 'qi' ],
|
||||
rsaKey = [];
|
||||
for ( var i = 0; i < rsaComp.length; i++ ) {
|
||||
if ( !( rsaComp[i] in k ) ) break;
|
||||
var b = rsaKey[i] = s2b( a2s( k[ rsaComp[i] ] ) );
|
||||
if ( b[0] & 0x80 ) rsaKey[i] = new Uint8Array(b.length + 1), rsaKey[i].set( b, 1 );
|
||||
}
|
||||
if ( rsaKey.length > 2 ) prv = true, rsaKey.unshift( new Uint8Array([0]) ); // add version to PKCS#1 RSAPrivateKey structure
|
||||
info[0][0] = '1.2.840.113549.1.1.1';
|
||||
key = rsaKey;
|
||||
break;
|
||||
default:
|
||||
throw new TypeError("Unsupported key type");
|
||||
}
|
||||
info.push( new Uint8Array( der2b(key) ).buffer );
|
||||
if ( !prv ) info[1] = { 'tag': 0x03, 'value': info[1] };
|
||||
else info.unshift( new Uint8Array([0]) ); // add version to PKCS#8 PrivateKeyInfo structure
|
||||
return new Uint8Array( der2b(info) ).buffer;
|
||||
}
|
||||
|
||||
var oid2str = { 'KoZIhvcNAQEB': '1.2.840.113549.1.1.1' },
|
||||
str2oid = { '1.2.840.113549.1.1.1': 'KoZIhvcNAQEB' };
|
||||
|
||||
function b2der ( buf, ctx ) {
|
||||
if ( buf instanceof ArrayBuffer ) buf = new Uint8Array(buf);
|
||||
if ( !ctx ) ctx = { pos: 0, end: buf.length };
|
||||
|
||||
if ( ctx.end - ctx.pos < 2 || ctx.end > buf.length ) throw new RangeError("Malformed DER");
|
||||
|
||||
var tag = buf[ctx.pos++],
|
||||
len = buf[ctx.pos++];
|
||||
|
||||
if ( len >= 0x80 ) {
|
||||
len &= 0x7f;
|
||||
if ( ctx.end - ctx.pos < len ) throw new RangeError("Malformed DER");
|
||||
for ( var xlen = 0; len--; ) xlen <<= 8, xlen |= buf[ctx.pos++];
|
||||
len = xlen;
|
||||
}
|
||||
|
||||
if ( ctx.end - ctx.pos < len ) throw new RangeError("Malformed DER");
|
||||
|
||||
var rv;
|
||||
|
||||
switch ( tag ) {
|
||||
case 0x02: // Universal Primitive INTEGER
|
||||
rv = buf.subarray( ctx.pos, ctx.pos += len );
|
||||
break;
|
||||
case 0x03: // Universal Primitive BIT STRING
|
||||
if ( buf[ctx.pos++] ) throw new Error( "Unsupported bit string" );
|
||||
len--;
|
||||
case 0x04: // Universal Primitive OCTET STRING
|
||||
rv = new Uint8Array( buf.subarray( ctx.pos, ctx.pos += len ) ).buffer;
|
||||
break;
|
||||
case 0x05: // Universal Primitive NULL
|
||||
rv = null;
|
||||
break;
|
||||
case 0x06: // Universal Primitive OBJECT IDENTIFIER
|
||||
var oid = btoa( b2s( buf.subarray( ctx.pos, ctx.pos += len ) ) );
|
||||
if ( !( oid in oid2str ) ) throw new Error( "Unsupported OBJECT ID " + oid );
|
||||
rv = oid2str[oid];
|
||||
break;
|
||||
case 0x30: // Universal Constructed SEQUENCE
|
||||
rv = [];
|
||||
for ( var end = ctx.pos + len; ctx.pos < end; ) rv.push( b2der( buf, ctx ) );
|
||||
break;
|
||||
default:
|
||||
throw new Error( "Unsupported DER tag 0x" + tag.toString(16) );
|
||||
}
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
function der2b ( val, buf ) {
|
||||
if ( !buf ) buf = [];
|
||||
|
||||
var tag = 0, len = 0,
|
||||
pos = buf.length + 2;
|
||||
|
||||
buf.push( 0, 0 ); // placeholder
|
||||
|
||||
if ( val instanceof Uint8Array ) { // Universal Primitive INTEGER
|
||||
tag = 0x02, len = val.length;
|
||||
for ( var i = 0; i < len; i++ ) buf.push( val[i] );
|
||||
}
|
||||
else if ( val instanceof ArrayBuffer ) { // Universal Primitive OCTET STRING
|
||||
tag = 0x04, len = val.byteLength, val = new Uint8Array(val);
|
||||
for ( var i = 0; i < len; i++ ) buf.push( val[i] );
|
||||
}
|
||||
else if ( val === null ) { // Universal Primitive NULL
|
||||
tag = 0x05, len = 0;
|
||||
}
|
||||
else if ( typeof val === 'string' && val in str2oid ) { // Universal Primitive OBJECT IDENTIFIER
|
||||
var oid = s2b( atob( str2oid[val] ) );
|
||||
tag = 0x06, len = oid.length;
|
||||
for ( var i = 0; i < len; i++ ) buf.push( oid[i] );
|
||||
}
|
||||
else if ( val instanceof Array ) { // Universal Constructed SEQUENCE
|
||||
for ( var i = 0; i < val.length; i++ ) der2b( val[i], buf );
|
||||
tag = 0x30, len = buf.length - pos;
|
||||
}
|
||||
else if ( typeof val === 'object' && val.tag === 0x03 && val.value instanceof ArrayBuffer ) { // Tag hint
|
||||
val = new Uint8Array(val.value), tag = 0x03, len = val.byteLength;
|
||||
buf.push(0); for ( var i = 0; i < len; i++ ) buf.push( val[i] );
|
||||
len++;
|
||||
}
|
||||
else {
|
||||
throw new Error( "Unsupported DER value " + val );
|
||||
}
|
||||
|
||||
if ( len >= 0x80 ) {
|
||||
var xlen = len, len = 4;
|
||||
buf.splice( pos, 0, (xlen >> 24) & 0xff, (xlen >> 16) & 0xff, (xlen >> 8) & 0xff, xlen & 0xff );
|
||||
while ( len > 1 && !(xlen >> 24) ) xlen <<= 8, len--;
|
||||
if ( len < 4 ) buf.splice( pos, 4 - len );
|
||||
len |= 0x80;
|
||||
}
|
||||
|
||||
buf.splice( pos - 2, 2, tag, len );
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
function CryptoKey ( key, alg, ext, use ) {
|
||||
Object.defineProperties( this, {
|
||||
_key: {
|
||||
value: key
|
||||
},
|
||||
type: {
|
||||
value: key.type,
|
||||
enumerable: true,
|
||||
},
|
||||
extractable: {
|
||||
value: (ext === undefined) ? key.extractable : ext,
|
||||
enumerable: true,
|
||||
},
|
||||
algorithm: {
|
||||
value: (alg === undefined) ? key.algorithm : alg,
|
||||
enumerable: true,
|
||||
},
|
||||
usages: {
|
||||
value: (use === undefined) ? key.usages : use,
|
||||
enumerable: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function isPubKeyUse ( u ) {
|
||||
return u === 'verify' || u === 'encrypt' || u === 'wrapKey';
|
||||
}
|
||||
|
||||
function isPrvKeyUse ( u ) {
|
||||
return u === 'sign' || u === 'decrypt' || u === 'unwrapKey';
|
||||
}
|
||||
|
||||
[ 'generateKey', 'importKey', 'unwrapKey' ]
|
||||
.forEach( function ( m ) {
|
||||
var _fn = _subtle[m];
|
||||
|
||||
_subtle[m] = function ( a, b, c ) {
|
||||
var args = [].slice.call(arguments),
|
||||
ka, kx, ku;
|
||||
|
||||
switch ( m ) {
|
||||
case 'generateKey':
|
||||
ka = alg(a), kx = b, ku = c;
|
||||
break;
|
||||
case 'importKey':
|
||||
ka = alg(c), kx = args[3], ku = args[4];
|
||||
if ( a === 'jwk' ) {
|
||||
b = b2jwk(b);
|
||||
if ( !b.alg ) b.alg = jwkAlg(ka);
|
||||
if ( !b.key_ops ) b.key_ops = ( b.kty !== 'oct' ) ? ( 'd' in b ) ? ku.filter(isPrvKeyUse) : ku.filter(isPubKeyUse) : ku.slice();
|
||||
args[1] = jwk2b(b);
|
||||
}
|
||||
break;
|
||||
case 'unwrapKey':
|
||||
ka = args[4], kx = args[5], ku = args[6];
|
||||
args[2] = c._key;
|
||||
break;
|
||||
}
|
||||
|
||||
if ( m === 'generateKey' && ka.name === 'HMAC' && ka.hash ) {
|
||||
ka.length = ka.length || { 'SHA-1': 512, 'SHA-256': 512, 'SHA-384': 1024, 'SHA-512': 1024 }[ka.hash.name];
|
||||
return _subtle.importKey( 'raw', _crypto.getRandomValues( new Uint8Array( (ka.length+7)>>3 ) ), ka, kx, ku );
|
||||
}
|
||||
|
||||
if ( isWebkit && m === 'generateKey' && ka.name === 'RSASSA-PKCS1-v1_5' && ( !ka.modulusLength || ka.modulusLength >= 2048 ) ) {
|
||||
a = alg(a), a.name = 'RSAES-PKCS1-v1_5', delete a.hash;
|
||||
return _subtle.generateKey( a, true, [ 'encrypt', 'decrypt' ] )
|
||||
.then( function ( k ) {
|
||||
return Promise.all([
|
||||
_subtle.exportKey( 'jwk', k.publicKey ),
|
||||
_subtle.exportKey( 'jwk', k.privateKey ),
|
||||
]);
|
||||
})
|
||||
.then( function ( keys ) {
|
||||
keys[0].alg = keys[1].alg = jwkAlg(ka);
|
||||
keys[0].key_ops = ku.filter(isPubKeyUse), keys[1].key_ops = ku.filter(isPrvKeyUse);
|
||||
return Promise.all([
|
||||
_subtle.importKey( 'jwk', keys[0], ka, kx, keys[0].key_ops ),
|
||||
_subtle.importKey( 'jwk', keys[1], ka, kx, keys[1].key_ops ),
|
||||
]);
|
||||
})
|
||||
.then( function ( keys ) {
|
||||
return {
|
||||
publicKey: keys[0],
|
||||
privateKey: keys[1],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if ( ( isWebkit || ( isIE && ( ka.hash || {} ).name === 'SHA-1' ) )
|
||||
&& m === 'importKey' && a === 'jwk' && ka.name === 'HMAC' && b.kty === 'oct' ) {
|
||||
return _subtle.importKey( 'raw', s2b( a2s(b.k) ), c, args[3], args[4] );
|
||||
}
|
||||
|
||||
if ( isWebkit && m === 'importKey' && ( a === 'spki' || a === 'pkcs8' ) ) {
|
||||
return _subtle.importKey( 'jwk', pkcs2jwk(b), c, args[3], args[4] );
|
||||
}
|
||||
|
||||
if ( isIE && m === 'unwrapKey' ) {
|
||||
return _subtle.decrypt( args[3], c, b )
|
||||
.then( function ( k ) {
|
||||
return _subtle.importKey( a, k, args[4], args[5], args[6] );
|
||||
});
|
||||
}
|
||||
|
||||
var op;
|
||||
try {
|
||||
op = _fn.apply( _subtle, args );
|
||||
}
|
||||
catch ( e ) {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
|
||||
if ( isIE ) {
|
||||
op = new Promise( function ( res, rej ) {
|
||||
op.onabort =
|
||||
op.onerror = function ( e ) { rej(e) };
|
||||
op.oncomplete = function ( r ) { res(r.target.result) };
|
||||
});
|
||||
}
|
||||
|
||||
op = op.then( function ( k ) {
|
||||
if ( ka.name === 'HMAC' ) {
|
||||
if ( !ka.length ) ka.length = 8 * k.algorithm.length;
|
||||
}
|
||||
if ( ka.name.search('RSA') == 0 ) {
|
||||
if ( !ka.modulusLength ) ka.modulusLength = (k.publicKey || k).algorithm.modulusLength;
|
||||
if ( !ka.publicExponent ) ka.publicExponent = (k.publicKey || k).algorithm.publicExponent;
|
||||
}
|
||||
if ( k.publicKey && k.privateKey ) {
|
||||
k = {
|
||||
publicKey: new CryptoKey( k.publicKey, ka, kx, ku.filter(isPubKeyUse) ),
|
||||
privateKey: new CryptoKey( k.privateKey, ka, kx, ku.filter(isPrvKeyUse) ),
|
||||
};
|
||||
}
|
||||
else {
|
||||
k = new CryptoKey( k, ka, kx, ku );
|
||||
}
|
||||
return k;
|
||||
});
|
||||
|
||||
return op;
|
||||
}
|
||||
});
|
||||
|
||||
[ 'exportKey', 'wrapKey' ]
|
||||
.forEach( function ( m ) {
|
||||
var _fn = _subtle[m];
|
||||
|
||||
_subtle[m] = function ( a, b, c ) {
|
||||
var args = [].slice.call(arguments);
|
||||
|
||||
switch ( m ) {
|
||||
case 'exportKey':
|
||||
args[1] = b._key;
|
||||
break;
|
||||
case 'wrapKey':
|
||||
args[1] = b._key, args[2] = c._key;
|
||||
break;
|
||||
}
|
||||
|
||||
if ( ( isWebkit || ( isIE && ( b.algorithm.hash || {} ).name === 'SHA-1' ) )
|
||||
&& m === 'exportKey' && a === 'jwk' && b.algorithm.name === 'HMAC' ) {
|
||||
args[0] = 'raw';
|
||||
}
|
||||
|
||||
if ( isWebkit && m === 'exportKey' && ( a === 'spki' || a === 'pkcs8' ) ) {
|
||||
args[0] = 'jwk';
|
||||
}
|
||||
|
||||
if ( isIE && m === 'wrapKey' ) {
|
||||
return _subtle.exportKey( a, b )
|
||||
.then( function ( k ) {
|
||||
if ( a === 'jwk' ) k = s2b( unescape( encodeURIComponent( JSON.stringify( b2jwk(k) ) ) ) );
|
||||
return _subtle.encrypt( args[3], c, k );
|
||||
});
|
||||
}
|
||||
|
||||
var op;
|
||||
try {
|
||||
op = _fn.apply( _subtle, args );
|
||||
}
|
||||
catch ( e ) {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
|
||||
if ( isIE ) {
|
||||
op = new Promise( function ( res, rej ) {
|
||||
op.onabort =
|
||||
op.onerror = function ( e ) { rej(e) };
|
||||
op.oncomplete = function ( r ) { res(r.target.result) };
|
||||
});
|
||||
}
|
||||
|
||||
if ( m === 'exportKey' && a === 'jwk' ) {
|
||||
op = op.then( function ( k ) {
|
||||
if ( ( isWebkit || ( isIE && ( b.algorithm.hash || {} ).name === 'SHA-1' ) )
|
||||
&& b.algorithm.name === 'HMAC') {
|
||||
return { 'kty': 'oct', 'alg': jwkAlg(b.algorithm), 'key_ops': b.usages.slice(), 'ext': true, 'k': s2a( b2s(k) ) };
|
||||
}
|
||||
k = b2jwk(k);
|
||||
if ( !k.alg ) k['alg'] = jwkAlg(b.algorithm);
|
||||
if ( !k.key_ops ) k['key_ops'] = ( b.type === 'public' ) ? b.usages.filter(isPubKeyUse) : ( b.type === 'private' ) ? b.usages.filter(isPrvKeyUse) : b.usages.slice();
|
||||
return k;
|
||||
});
|
||||
}
|
||||
|
||||
if ( isWebkit && m === 'exportKey' && ( a === 'spki' || a === 'pkcs8' ) ) {
|
||||
op = op.then( function ( k ) {
|
||||
k = jwk2pkcs( b2jwk(k) );
|
||||
return k;
|
||||
});
|
||||
}
|
||||
|
||||
return op;
|
||||
}
|
||||
});
|
||||
|
||||
[ 'encrypt', 'decrypt', 'sign', 'verify' ]
|
||||
.forEach( function ( m ) {
|
||||
var _fn = _subtle[m];
|
||||
|
||||
_subtle[m] = function ( a, b, c, d ) {
|
||||
if ( isIE && ( !c.byteLength || ( d && !d.byteLength ) ) )
|
||||
throw new Error("Empy input is not allowed");
|
||||
|
||||
var args = [].slice.call(arguments),
|
||||
ka = alg(a);
|
||||
|
||||
if ( isIE && m === 'decrypt' && ka.name === 'AES-GCM' ) {
|
||||
var tl = a.tagLength >> 3;
|
||||
args[2] = (c.buffer || c).slice( 0, c.byteLength - tl ),
|
||||
a.tag = (c.buffer || c).slice( c.byteLength - tl );
|
||||
}
|
||||
|
||||
args[1] = b._key;
|
||||
|
||||
var op;
|
||||
try {
|
||||
op = _fn.apply( _subtle, args );
|
||||
}
|
||||
catch ( e ) {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
|
||||
if ( isIE ) {
|
||||
op = new Promise( function ( res, rej ) {
|
||||
op.onabort =
|
||||
op.onerror = function ( e ) {
|
||||
rej(e);
|
||||
};
|
||||
|
||||
op.oncomplete = function ( r ) {
|
||||
var r = r.target.result;
|
||||
|
||||
if ( m === 'encrypt' && r instanceof AesGcmEncryptResult ) {
|
||||
var c = r.ciphertext, t = r.tag;
|
||||
r = new Uint8Array( c.byteLength + t.byteLength );
|
||||
r.set( new Uint8Array(c), 0 );
|
||||
r.set( new Uint8Array(t), c.byteLength );
|
||||
r = r.buffer;
|
||||
}
|
||||
|
||||
res(r);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return op;
|
||||
}
|
||||
});
|
||||
|
||||
if ( isIE ) {
|
||||
var _digest = _subtle.digest;
|
||||
|
||||
_subtle['digest'] = function ( a, b ) {
|
||||
if ( !b.byteLength )
|
||||
throw new Error("Empy input is not allowed");
|
||||
|
||||
var op;
|
||||
try {
|
||||
op = _digest.call( _subtle, a, b );
|
||||
}
|
||||
catch ( e ) {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
|
||||
op = new Promise( function ( res, rej ) {
|
||||
op.onabort =
|
||||
op.onerror = function ( e ) { rej(e) };
|
||||
op.oncomplete = function ( r ) { res(r.target.result) };
|
||||
});
|
||||
|
||||
return op;
|
||||
};
|
||||
|
||||
global.crypto = Object.create( _crypto, {
|
||||
getRandomValues: { value: function ( a ) { return _crypto.getRandomValues(a) } },
|
||||
subtle: { value: _subtle },
|
||||
});
|
||||
|
||||
global.CryptoKey = CryptoKey;
|
||||
}
|
||||
|
||||
if ( isWebkit ) {
|
||||
_crypto.subtle = _subtle;
|
||||
|
||||
global.Crypto = _Crypto;
|
||||
global.SubtleCrypto = _SubtleCrypto;
|
||||
global.CryptoKey = CryptoKey;
|
||||
}
|
||||
}(this);
|
@ -169,6 +169,7 @@
|
||||
<script src="/vendor/modernizr-custom.min.js"></script>
|
||||
<script src="/vendor/autosize.min.js"></script>
|
||||
<script src="/vendor/bootstrap-switch.min.js"></script>
|
||||
<script src="/vendor/web-crypto-shim.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/fastclick/1.0.6/fastclick.min.js"></script>
|
||||
<script src="/main.js"></script>
|
||||
<script>
|
||||
|
Loading…
x
Reference in New Issue
Block a user