UNPKG

etebase

Version:

Etebase TypeScript API for the web and node

235 lines 10.2 kB
// Shim document if it doesn't exist (e.g. on React native) if ((typeof global !== "undefined") && !global.document) { global.document = {}; } import _sodium from "libsodium-wrappers"; import * as Argon2 from "argon2-webworker"; import * as Constants from "./Constants"; import { numToUint8Array, symmetricNonceSize } from "./Helpers"; import { Rollsum } from "./Chunker"; import { ProgrammingError } from "./Exceptions"; const sodium = _sodium; let rnsodium; export function _setRnSodium(rnsodium_) { rnsodium = rnsodium_; } export const ready = (async () => { await sodium.ready; })(); export function concatArrayBuffers(buffer1, buffer2) { const ret = new Uint8Array(buffer1.length + buffer2.length); ret.set(buffer1, 0); ret.set(buffer2, buffer1.length); return ret; } export function concatArrayBuffersArrays(buffers) { const length = buffers.reduce((x, y) => x + y.length, 0); const ret = new Uint8Array(length); let pos = 0; for (const buffer of buffers) { ret.set(buffer, pos); pos += buffer.length; } return ret; } export var KeyDerivationDifficulty; (function (KeyDerivationDifficulty) { KeyDerivationDifficulty[KeyDerivationDifficulty["Hard"] = 90] = "Hard"; KeyDerivationDifficulty[KeyDerivationDifficulty["Medium"] = 50] = "Medium"; KeyDerivationDifficulty[KeyDerivationDifficulty["Easy"] = 10] = "Easy"; })(KeyDerivationDifficulty || (KeyDerivationDifficulty = {})); export async function deriveKey(salt, password, difficulty = KeyDerivationDifficulty.Hard) { salt = salt.subarray(0, sodium.crypto_pwhash_SALTBYTES); let opslimit; switch (difficulty) { case KeyDerivationDifficulty.Hard: opslimit = sodium.crypto_pwhash_OPSLIMIT_SENSITIVE; break; case KeyDerivationDifficulty.Medium: opslimit = sodium.crypto_pwhash_OPSLIMIT_MODERATE; break; case KeyDerivationDifficulty.Easy: opslimit = sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE; break; default: throw new ProgrammingError("Passed invalid difficulty."); } try { const ret = await Argon2.hash({ hashLen: 32, pass: password, salt, time: opslimit, mem: sodium.crypto_pwhash_MEMLIMIT_MODERATE / 1024, parallelism: 1, type: Argon2.ArgonType.Argon2id, }); return ret.hash; } catch (e) { if (typeof (Worker) !== "undefined") { // Web worker failed console.warn("Failed loading web worker!", e); } } if (rnsodium) { const ret = await rnsodium.crypto_pwhash(32, sodium.to_base64(sodium.from_string(password), sodium.base64_variants.ORIGINAL), sodium.to_base64(salt, sodium.base64_variants.ORIGINAL), opslimit, sodium.crypto_pwhash_MEMLIMIT_MODERATE, sodium.crypto_pwhash_ALG_DEFAULT); return sodium.from_base64(ret, sodium.base64_variants.ORIGINAL); } return sodium.crypto_pwhash(32, sodium.from_string(password), salt, opslimit, sodium.crypto_pwhash_MEMLIMIT_MODERATE, sodium.crypto_pwhash_ALG_DEFAULT); } export class CryptoManager { constructor(key, keyContext, version = Constants.CURRENT_VERSION) { keyContext = keyContext.padEnd(8); this.version = version; this.cipherKey = sodium.crypto_kdf_derive_from_key(32, 1, keyContext, key); this.macKey = sodium.crypto_kdf_derive_from_key(32, 2, keyContext, key); this.asymKeySeed = sodium.crypto_kdf_derive_from_key(32, 3, keyContext, key); this.subDerivationKey = sodium.crypto_kdf_derive_from_key(32, 4, keyContext, key); this.determinsticEncryptionKey = sodium.crypto_kdf_derive_from_key(32, 5, keyContext, key); } encrypt(message, additionalData = null) { const nonce = sodium.randombytes_buf(symmetricNonceSize); return concatArrayBuffers(nonce, sodium.crypto_aead_xchacha20poly1305_ietf_encrypt(message, additionalData, null, nonce, this.cipherKey)); } decrypt(nonceCiphertext, additionalData = null) { const nonce = nonceCiphertext.subarray(0, symmetricNonceSize); const ciphertext = nonceCiphertext.subarray(symmetricNonceSize); return sodium.crypto_aead_xchacha20poly1305_ietf_decrypt(null, ciphertext, additionalData, nonce, this.cipherKey); } encryptDetached(message, additionalData = null) { const nonce = sodium.randombytes_buf(symmetricNonceSize); const ret = sodium.crypto_aead_xchacha20poly1305_ietf_encrypt_detached(message, additionalData, null, nonce, this.cipherKey); return [ret.mac, concatArrayBuffers(nonce, ret.ciphertext)]; } decryptDetached(nonceCiphertext, mac, additionalData = null) { const nonce = nonceCiphertext.subarray(0, symmetricNonceSize); const ciphertext = nonceCiphertext.subarray(symmetricNonceSize); return sodium.crypto_aead_xchacha20poly1305_ietf_decrypt_detached(null, ciphertext, mac, additionalData, nonce, this.cipherKey); } verify(nonceCiphertext, mac, additionalData = null) { const nonce = nonceCiphertext.subarray(0, symmetricNonceSize); const ciphertext = nonceCiphertext.subarray(symmetricNonceSize); sodium.crypto_aead_xchacha20poly1305_ietf_decrypt_detached(null, ciphertext, mac, additionalData, nonce, this.cipherKey, null); return true; } deterministicEncrypt(message, additionalData = null) { // FIXME: we could me slightly more efficient (save 8 bytes) and use crypto_stream_xchacha20_xor directly, and // just have the mac be used to verify. Though that function is not exposed in libsodium.js (the slimmer version), // and it's easier to get wrong, so we are just using the full xchacha20poly1305 we already use anyway. const nonce = this.calculateMac(message).subarray(0, symmetricNonceSize); return concatArrayBuffers(nonce, sodium.crypto_aead_xchacha20poly1305_ietf_encrypt(message, additionalData, null, nonce, this.determinsticEncryptionKey)); } deterministicDecrypt(nonceCiphertext, additionalData = null) { const nonce = nonceCiphertext.subarray(0, symmetricNonceSize); const ciphertext = nonceCiphertext.subarray(symmetricNonceSize); return sodium.crypto_aead_xchacha20poly1305_ietf_decrypt(null, ciphertext, additionalData, nonce, this.determinsticEncryptionKey); } deriveSubkey(salt) { return sodium.crypto_generichash(32, this.subDerivationKey, salt); } getCryptoMac(withKey = true) { const key = (withKey) ? this.macKey : null; return new CryptoMac(key); } calculateMac(message, withKey = true) { const key = (withKey) ? this.macKey : null; return sodium.crypto_generichash(32, message, key); } getChunker() { return new Rollsum(); } } export class LoginCryptoManager { constructor(keypair) { this.keypair = keypair; } static keygen(seed) { return new this(sodium.crypto_sign_seed_keypair(seed)); } signDetached(message) { return sodium.crypto_sign_detached(message, this.keypair.privateKey); } static verifyDetached(message, signature, pubkey) { return sodium.crypto_sign_verify_detached(signature, message, pubkey); } get pubkey() { return this.keypair.publicKey; } } export class BoxCryptoManager { constructor(keypair) { this.keypair = keypair; } static keygen(seed) { if (seed) { return new this(sodium.crypto_box_seed_keypair(seed)); } else { return new this(sodium.crypto_box_keypair()); } } static fromPrivkey(privkey) { return new this({ keyType: "x25519", privateKey: privkey, publicKey: sodium.crypto_scalarmult_base(privkey), }); } encrypt(message, pubkey) { const nonce = sodium.randombytes_buf(sodium.crypto_box_NONCEBYTES); const ret = sodium.crypto_box_easy(message, nonce, pubkey, this.keypair.privateKey); return concatArrayBuffers(nonce, ret); } decrypt(nonceCiphertext, pubkey) { const nonceSize = sodium.crypto_box_NONCEBYTES; const nonce = nonceCiphertext.subarray(0, nonceSize); const ciphertext = nonceCiphertext.subarray(nonceSize); return sodium.crypto_box_open_easy(ciphertext, nonce, pubkey, this.keypair.privateKey); } get pubkey() { return this.keypair.publicKey; } get privkey() { return this.keypair.privateKey; } } export class CryptoMac { constructor(key, length = 32) { this.length = length; this.state = sodium.crypto_generichash_init(key, length); } updateWithLenPrefix(messageChunk) { sodium.crypto_generichash_update(this.state, numToUint8Array(messageChunk.length)); sodium.crypto_generichash_update(this.state, messageChunk); } update(messageChunk) { sodium.crypto_generichash_update(this.state, messageChunk); } finalize() { return sodium.crypto_generichash_final(this.state, this.length); } } function getEncodedChunk(content, offset) { const num = ((content[offset] << 16) + (content[offset + 1] << 8) + content[offset + 2]) % 100000; return num.toString().padStart(5, "0"); } export function getPrettyFingerprint(content, delimiter = " ") { const fingerprint = sodium.crypto_generichash(32, content); /* We use 3 bytes each time to generate a 5 digit number - this means 10 pairs for bytes 0-29 * We then use bytes 29-31 for another number, and then the 3 most significant bits of each first byte for the last. */ let ret = ""; let lastNum = 0; for (let i = 0; i < 10; i++) { const suffix = (i % 4 === 3) ? "\n" : delimiter; ret += getEncodedChunk(fingerprint, i * 3) + suffix; lastNum = (lastNum << 3) | ((fingerprint[i] & 0xE0) >>> 5); } ret += getEncodedChunk(fingerprint, 29) + delimiter; ret += (lastNum % 100000).toString().padStart(5, "0"); return ret; } //# sourceMappingURL=Crypto.js.map