commlink
Version:
JavaScript Encrypted Communications
278 lines (238 loc) • 8.54 kB
JavaScript
'use strict';
function Commlink(crypto) {
let commlink = {};
if (typeof window !== 'undefined') {
crypto = window.crypto || crypto;
}
const toHex = commlink.toHex = (byteArray) => {
return Array.from(new Uint8Array(byteArray)).map(val => {
return ('0' + val.toString(16)).slice(-2);
}).join('');
};
const fromHex = commlink.fromHex = (str) => {
let result = new Uint8Array(str.match(/.{0,2}/g).map(val => {
return parseInt(val, 16);
}));
return result.slice(0, result.length - 1);
};
const encode = commlink.encode = (byteArray) => {
return btoa(Array.from(new Uint8Array(byteArray)).map(val => {
return String.fromCharCode(val);
}).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/\=/g, '');
};
const decode = commlink.decode = (str) => {
return new Uint8Array(atob(str.replace(/\_/g, '/').replace(/\-/g, '+')).split('').map(val => {
return val.charCodeAt(0);
}));
};
const fromText = commlink.fromText = (string) => {
return new Uint8Array(string.split('').map(val => {
return val.charCodeAt(0);
}));
};
const toText = commlink.toText = (byteArray) => {
return Array.from(new Uint8Array(byteArray)).map(val => {
return String.fromCharCode(val);
}).join('');
};
const combine = commlink.combine = (bitsA = [], bitsB = []) => {
let A = bitsA;
let B = bitsB;
if (typeof bitsA === 'string') {
A = decode(bitsA);
}
if (typeof bitsB === 'string') {
B = decode(bitsB);
}
let a = new Uint8Array(A);
let b = new Uint8Array(B);
let c = new Uint8Array(a.length + b.length);
c.set(a);
c.set(b, a.length);
return c;
};
const random = commlink.random = (size) => {
return crypto.getRandomValues(new Uint8Array(size));
};
const createECDH = commlink.createECDH = async (curve = "P-256") => {
let DH = await crypto.subtle.generateKey({
"name": "ECDH",
"namedCurve": curve
}, true, ['deriveBits']);
let pub = await crypto.subtle.exportKey('raw', DH.publicKey);
let key = encode(await crypto.subtle.exportKey('pkcs8', DH.privateKey));
return {
"pub": encode(pub),
"key": key
};
};
const createECDSA = commlink.createECDSA = async (curve = "P-256") => {
let user = await crypto.subtle.generateKey({
"name": "ECDSA",
"namedCurve": curve
}, true, ['sign', 'verify']);
let pub = await crypto.subtle.exportKey('raw', user.publicKey);
let key = encode(await crypto.subtle.exportKey('pkcs8', user.privateKey));
return {
"pub": encode(pub),
"key": key
};
};
const ecdsaSign = commlink.ecdsaSign = commlink.sign = async (key, msg, curve = "P-256", hashAlg = "SHA-256") => {
let message = msg.toString();
let signKey = await crypto.subtle.importKey('pkcs8', decode(key), {
"name": "ECDSA",
"namedCurve": curve
}, false, ['sign']);
let sig = await crypto.subtle.sign({
"name": "ECDSA",
"hash": hashAlg
}, signKey, fromText(message));
return encode(sig);
};
const ecdsaVerify = commlink.ecdsaVerify = commlink.verify = async (pub, sig, msg, curve = "P-256", hashAlg = "SHA-256") => {
let message = msg.toString();
let verifyKey = await crypto.subtle.importKey('raw', decode(pub), {
"name": "ECDSA",
"namedCurve": curve
}, false, ['verify']);
let verified = await crypto.subtle.verify({
"name": "ECDSA",
"hash": hashAlg
}, verifyKey, decode(sig), fromText(message));
return verified;
};
const hmacSign = commlink.hmacSign = async (bits, msg, hashAlg="SHA-256") => {
let message = msg.toString();
let hmacKey = await crypto.subtle.importKey('raw', bits, {
"name": "HMAC",
"hash": hashAlg,
}, false, ['sign']);
let sig = await crypto.subtle.sign({
"name": "HMAC",
"hash": hashAlg
}, hmacKey, fromText(message));
return encode(sig);
};
const hmacVerify = commlink.hmacVerify = async (bits, sig, msg, hashAlg="SHA-256") => {
let message = msg.toString();
let verifyKey = await crypto.subtle.importKey('raw', bits, {
"name": "HMAC",
"hash": hashAlg,
}, false, ['verify']);
let verified = await crypto.subtle.verify({
"name": "HMAC",
"hash": hashAlg
}, verifyKey, decode(sig), fromText(message));
return verified;
};
const digest = commlink.digest = async (bits, hashAlg = "SHA-256") => {
let digest = await crypto.subtle.digest({
"name": hashAlg
}, bits);
return toHex(digest);
};
const pbkdf2 = commlink.pbkdf2 = async (bits, salt, iterations = 1, size = 256, hashAlg = "SHA-256") => {
let key = await crypto.subtle.importKey('raw', bits, {
"name": "PBKDF2"
}, false, ['deriveBits']);
let result = await crypto.subtle.deriveBits({
"name": "PBKDF2",
"salt": salt,
"iterations": iterations,
"hash": hashAlg
}, key, size);
return encode(result);
};
const hkdf = commlink.hkdf = async (bits, salt, info, size, hashAlg="SHA-256") => {
let ikm = bits;
let len = size;
let hashSize = 256;
if (hashAlg.toLocaleUpperCase() === 'SHA-512') {
hashSize = 512;
}
if (len > 255 * hashSize) {
throw("Error: Size exceeds maximum output length for selected hash.");
}
if (len < 8) {
throw("Error: Size cannot be smaller 8 bits.");
}
if (len / 8 !== parseInt(len / 8)) {
throw("Error: Size must be a multiple of 8 bits.");
}
let PRK = await hmacSign(salt, toText(ikm), hashAlg);
let result = new Uint8Array([]);
let T = new Uint8Array([]);
let rounds = Math.ceil(size / hashSize);
for (let i = 0; i < rounds; i++) {
let num = toText(new Uint8Array([i + 1]));
let t = toText(T) + toText(info);
let msg = t + num;
T = decode(await hmacSign(decode(PRK), msg, hashAlg));
result = combine(result, T);
}
return await encode(result.slice(0, len / 8));
}
const ecdh = commlink.ecdh = async (key, pub, curve = "P-256", size = 256) => {
let pubKey = await crypto.subtle.importKey('raw', decode(pub), {
"name": "ECDH",
"namedCurve": curve
}, true, []);
let privateKey = await crypto.subtle.importKey('pkcs8', decode(key), {
"name": "ECDH",
"namedCurve": curve
}, true, ['deriveBits']);
let shared = await crypto.subtle.deriveBits({
"name": "ECDH",
"public": pubKey
}, privateKey, size);
let bits = encode(shared);
return bits;
};
const encrypt = commlink.encrypt = async (plaintext, bits, AD = null) => {
let key = await crypto.subtle.importKey('raw', bits, {
"name": "AES-GCM"
}, false, ['encrypt']);
let iv = random(12);
let msg = fromText(plaintext);
let cipher = await crypto.subtle.encrypt({
"name": "AES-GCM",
"iv": iv,
"additionalData": AD || fromText('')
}, key, msg);
return encode(iv) + '.' + encode(cipher);
};
const decrypt = commlink.decrypt = async (ciphertext = "", bits, AD = null) => {
let key = await crypto.subtle.importKey('raw', bits, {
"name": "AES-GCM"
}, false, ['decrypt']);
let iv = decode(ciphertext.split('.')[0]);
let cipher = decode(ciphertext.split('.')[1]);
let decrypted = await crypto.subtle.decrypt({
"name": "AES-GCM",
"iv": iv,
"additionalData": AD || fromText('')
}, key, cipher).catch(err => {
throw({"message":"Failed to decrypt message.", "error":err});
});
return toText(decrypted);
};
const passwordEncrypt = commlink.passwordEncrypt = async (message, password = "", iterations = 100000) => {
let salt = random(32);
let keyBits = await pbkdf2(fromText(password), salt, iterations, 256);
let encrypted = await encrypt(message, decode(keyBits));
return encode(fromText(iterations.toString())) + '.' + encode(salt) + '.' + encrypted;
};
const passwordDecrypt = commlink.passwordDecrypt = async (ciphertext = "", password = "") => {
let iterations = toText(decode(ciphertext.split('.')[0]));
let salt = ciphertext.split('.')[1];
let keyBits = await pbkdf2(fromText(password), decode(salt), iterations, 256);
let encrypted = ciphertext.split('.').slice(2).join('.');
let decrypted = await decrypt(encrypted, decode(keyBits));
return decrypted;
};
return commlink;
}
if (typeof module !== 'undefined' && module && module.exports) {
module.exports = Commlink;
}