Initial commit: OCA Server Auth packages (29 packages)

This commit is contained in:
Ernad Husremovic 2025-08-29 15:43:06 +02:00
commit 3ed80311c4
1325 changed files with 127292 additions and 0 deletions

View file

@ -0,0 +1,576 @@
/** @odoo-module alias=vault.utils **/
// © 2021-2024 Florian Kantelberg - initOS GmbH
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import {_t, qweb} from "web.core";
import Dialog from "web.Dialog";
const CryptoAPI = window.crypto.subtle;
// Some basic constants used for the entire vaults
const Hash = "SHA-512";
const HashLength = 10;
const IVLength = 12;
const SaltLength = 32;
const Asymmetric = {
name: "RSA-OAEP",
modulusLength: 4096,
publicExponent: new Uint8Array([1, 0, 1]),
hash: Hash,
};
const Derive = {
name: "PBKDF2",
iterations: 600001,
};
const Symmetric = {
name: "AES-GCM",
length: 256,
};
/**
* Checks if the CryptoAPI is available and the vault feature supported
*
* @returns if vault is supported
*/
function supported() {
return Boolean(CryptoAPI);
}
/**
* Converts an ArrayBuffer to an ASCII string
*
* @param {ArrayBuffer} buffer
* @returns the data converted to a string
*/
function toBinary(buffer) {
if (!buffer) return "";
const chars = Array.from(new Uint8Array(buffer)).map(function (b) {
return String.fromCharCode(b);
});
return chars.join("");
}
/**
* Converts an ASCII string to an ArrayBuffer
*
* @param {String} binary
* @returns the data converted to an ArrayBuffer
*/
function fromBinary(binary) {
const len = binary.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) bytes[i] = binary.charCodeAt(i);
return bytes.buffer;
}
/**
* Converts an ArrayBuffer to a Base64 encoded string
*
* @param {ArrayBuffer} buffer
* @returns Base64 string
*/
function toBase64(buffer) {
if (!buffer) return "";
const chars = Array.from(new Uint8Array(buffer)).map(function (b) {
return String.fromCharCode(b);
});
return btoa(chars.join(""));
}
/**
* Converts an Base64 encoded string to an ArrayBuffer
*
* @param {String} base64
* @returns the data converted to an ArrayBuffer
*/
function fromBase64(base64) {
if (!base64) {
const bytes = new Uint8Array(0);
return bytes.buffer;
}
const binary_string = atob(base64);
const len = binary_string.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) bytes[i] = binary_string.charCodeAt(i);
return bytes.buffer;
}
/**
* Generate random bytes used for salts or IVs
*
* @param {int} length
* @returns an array with length random bytes
*/
function generate_bytes(length) {
const buf = new Uint8Array(length);
window.crypto.getRandomValues(buf);
return buf;
}
/**
* Generate random bytes used for salts or IVs encoded as base64
*
* @returns base64 string
*/
function generate_iv_base64() {
return toBase64(generate_bytes(IVLength));
}
/**
* Generate a random secret with specific characters
*
* @param {int} length
* @param {String} characters
* @returns base64 string
*/
function generate_secret(length, characters) {
let result = "";
const len = characters.length;
for (const k of generate_bytes(length))
result += characters[Math.floor((len * k) / 256)];
return result;
}
/**
* Generate a symmetric key for encrypting and decrypting
*
* @returns symmetric key
*/
async function generate_key() {
return await CryptoAPI.generateKey(Symmetric, true, ["encrypt", "decrypt"]);
}
/**
* Generate an asymmetric key pair for encrypting, decrypting, wrapping and unwrapping
*
* @returns asymmetric key
*/
async function generate_key_pair() {
return await CryptoAPI.generateKey(Asymmetric, true, [
"wrapKey",
"unwrapKey",
"decrypt",
"encrypt",
]);
}
/**
* Generate a hash of the given data
*
* @param {String} data
* @returns base64 encoded hash of the data
*/
async function digest(data) {
const encoder = new TextEncoder();
return toBase64(await CryptoAPI.digest(Hash, encoder.encode(data)));
}
/**
* Ask the user to enter a password using a dialog
*
* @param {String} title of the dialog
* @param {Object} options
* @returns promise
*/
function askpass(title, options = {}) {
var self = this;
if (options.password === undefined) options.password = true;
if (options.keyfile === undefined) options.keyfile = true;
return new Promise((resolve, reject) => {
var dialog = new Dialog(self, {
title: title,
$content: $(qweb.render("vault.askpass", options)),
buttons: [
{
text: _t("Enter"),
classes: "btn-primary",
click: async function (ev) {
ev.stopPropagation();
const password = this.$("#password").val();
const keyfile = this.$("#keyfile")[0].files[0];
if (!password && !keyfile) {
Dialog.alert(this, _t("Missing password"));
return;
}
if (options.confirm) {
const confirm = this.$("#confirm").val();
if (confirm !== password) {
Dialog.alert(this, _t("The passwords aren't matching"));
return;
}
}
dialog.close();
let keyfile_content = null;
if (keyfile) keyfile_content = fromBinary(await keyfile.text());
resolve({
password: password,
keyfile: keyfile_content,
});
},
},
{
text: _t("Cancel"),
click: function (ev) {
ev.stopPropagation();
dialog.close();
reject(_t("Cancelled"));
},
},
],
});
dialog.open();
});
}
/**
* Ask the user to enter a password using a dialog
*
* @param {String} title of the dialog
* @param {Object} options
* @returns promise
*/
function generate_pass(title, options = {}) {
var self = this;
const $content = $(qweb.render("vault.generate_pass", options));
const $password = $content.find("#password")[0];
const $length = $content.find("#length")[0];
const $big = $content.find("#big_letter")[0];
const $small = $content.find("#small_letter")[0];
const $digits = $content.find("#digits")[0];
const $special = $content.find("#special")[0];
var password = null;
function gen_pass() {
let characters = "";
if ($big.checked) characters += "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
if ($small.checked) characters += "abcdefghijklmnopqrstuvwxyz";
if ($digits.checked) characters += "0123456789";
if ($special.checked) characters += "!?$%&/()[]{}|<>,;.:-_#+*\\";
if (characters)
$password.innerHTML = password = generate_secret($length.value, characters);
}
$length.onchange =
$big.onchange =
$small.onchange =
$digits.onchange =
$special.onchange =
gen_pass;
gen_pass();
return new Promise((resolve, reject) => {
var dialog = new Dialog(self, {
title: title,
$content: $content,
buttons: [
{
text: _t("Enter"),
classes: "btn-primary",
click: async function (ev) {
ev.stopPropagation();
if (!password) throw new Error(_t("Missing password"));
dialog.close();
resolve(password);
},
},
{
text: _t("Cancel"),
click: function (ev) {
ev.stopPropagation();
dialog.close();
reject(_t("Cancelled"));
},
},
],
});
dialog.open();
});
}
/**
* Derive a key using the given data, salt and iterations using PBKDF2
*
* @param {String} data
* @param {String} salt
* @param {int} iterations
* @returns the derived key
*/
async function derive_key(data, salt, iterations) {
const enc = new TextEncoder();
const material = await CryptoAPI.importKey(
"raw",
enc.encode(data),
Derive.name,
false,
["deriveBits", "deriveKey"]
);
return await CryptoAPI.deriveKey(
{
name: Derive.name,
salt: salt,
iterations: iterations,
hash: Hash,
},
material,
Symmetric,
false,
["wrapKey", "unwrapKey", "encrypt", "decrypt"]
);
}
/**
* Encrypt the data using a public key
*
* @param {CryptoKey} public_key
* @param {String} data
* @returns the encrypted data
*/
async function asym_encrypt(public_key, data) {
if (!data) return data;
const enc = new TextEncoder();
return toBase64(
await CryptoAPI.encrypt({name: Asymmetric.name}, public_key, enc.encode(data))
);
}
/**
* Decrypt the data using the own private key
*
* @param {CryptoKey} private_key
* @param {String} crypted
* @returns the decrypted data
*/
async function asym_decrypt(private_key, crypted) {
if (!crypted) return crypted;
const dec = new TextDecoder();
return dec.decode(
await CryptoAPI.decrypt(
{name: Asymmetric.name},
private_key,
fromBase64(crypted)
)
);
}
/**
* Symmetrically encrypt the data using a master key
*
* @param {CryptoKey} key
* @param {String} data
* @param {String} iv
* @returns the encrypted data
*/
async function sym_encrypt(key, data, iv) {
if (!data) return data;
const hash = await digest(data);
const enc = new TextEncoder();
return toBase64(
await CryptoAPI.encrypt(
{name: Symmetric.name, iv: fromBase64(iv), tagLength: 128},
key,
enc.encode(hash.slice(0, HashLength) + data)
)
);
}
/**
* Symmetrically decrypt the data using a master key
*
* @param {CryptoKey} key
* @param {String} crypted
* @param {String} iv
* @returns the decrypted data
*/
async function sym_decrypt(key, crypted, iv) {
if (!crypted) return crypted;
try {
const dec = new TextDecoder();
const message = dec.decode(
await CryptoAPI.decrypt(
{name: Symmetric.name, iv: fromBase64(iv), tagLength: 128},
key,
fromBase64(crypted)
)
);
const data = message.slice(HashLength);
const hash = await digest(data);
// Compare the hash and return if integer
if (hash.slice(0, HashLength) === message.slice(0, HashLength)) return data;
console.error("Invalid data hash");
// Wrong hash
return null;
} catch (err) {
console.error(err);
return null;
}
}
/**
* Load a public key
*
* @param {String} public_key
* @returns the public key as CryptoKey
*/
async function load_public_key(public_key) {
return await CryptoAPI.importKey("spki", fromBase64(public_key), Asymmetric, true, [
"wrapKey",
"encrypt",
]);
}
/**
* Load a private key
*
* @param {String} private_key
* @param {CryptoKey} key
* @param {String} iv
* @returns the private key as CryptoKey
*/
async function load_private_key(private_key, key, iv) {
return await CryptoAPI.unwrapKey(
"pkcs8",
fromBase64(private_key),
key,
{name: Symmetric.name, iv: fromBase64(iv), tagLength: 128},
Asymmetric,
true,
["unwrapKey", "decrypt"]
);
}
/**
* Export a public key in spki format
*
* @param {CryptoKey} public_key
* @returns the public key as string
*/
async function export_public_key(public_key) {
return toBase64(await CryptoAPI.exportKey("spki", public_key));
}
/**
* Export a private key in pkcs8 format
*
* @param {String} private_key
* @param {CryptoKey} key
* @param {String} iv
* @returns the public key as CryptoKey
*/
async function export_private_key(private_key, key, iv) {
return toBase64(
await CryptoAPI.wrapKey("pkcs8", private_key, key, {
name: Symmetric.name,
iv: iv,
tagLength: 128,
})
);
}
/**
* Wrap the master key with the own public key
*
* @param {CryptoKey} key
* @param {CryptoKey} public_key
* @returns wrapped master key
*/
async function wrap(key, public_key) {
return toBase64(await CryptoAPI.wrapKey("raw", key, public_key, Asymmetric));
}
/**
* Unwrap the master key with the own private key
*
* @param {CryptoKey} key
* @param {CryptoKey} private_key
* @returns unwrapped master key
*/
async function unwrap(key, private_key) {
return await CryptoAPI.unwrapKey(
"raw",
fromBase64(key),
private_key,
Asymmetric,
Symmetric,
true,
["encrypt", "decrypt"]
);
}
/**
* Capitalize each word of the string
*
* @param {String} s
* @returns capitalized string
*/
function capitalize(s) {
return s.toLowerCase().replace(/\b\w/g, function (c) {
return c.toUpperCase();
});
}
export default {
// Constants
Asymmetric: Asymmetric,
Derive: Derive,
Hash: Hash,
HashLength: HashLength,
IVLength: IVLength,
SaltLength: SaltLength,
Symmetric: Symmetric,
// Crypto utility functions
askpass: askpass,
asym_decrypt: asym_decrypt,
asym_encrypt: asym_encrypt,
derive_key: derive_key,
digest: digest,
export_private_key: export_private_key,
export_public_key: export_public_key,
generate_bytes: generate_bytes,
generate_iv_base64: generate_iv_base64,
generate_key: generate_key,
generate_key_pair: generate_key_pair,
generate_pass: generate_pass,
generate_secret: generate_secret,
load_private_key: load_private_key,
load_public_key: load_public_key,
sym_decrypt: sym_decrypt,
sym_encrypt: sym_encrypt,
unwrap: unwrap,
wrap: wrap,
// Utility functions
capitalize: capitalize,
fromBase64: fromBase64,
fromBinary: fromBinary,
toBase64: toBase64,
toBinary: toBinary,
supported: supported,
};