1/** 2 * Cifra hÃbrida (RSA-OAEP + AES-GCM) para as passwords não aparecerem em 3 * claro no corpo do pedido HTTP (visÃvel no separador "Rede" das DevTools, 4 * em proxies intermédios, em logs, etc.). 5 * 6 * Usa a biblioteca node-forge (criptografia pura em JavaScript) em vez da 7 * Web Crypto API (`window.crypto.subtle`), porque esta última só funciona 8 * em "contextos seguros" (HTTPS, ou localhost) â e este ficheiro pode 9 * correr em HTTP puro, inclusive por IP de rede local (ex. 192.168.x.x), 10 * onde `crypto.subtle` fica indisponÃvel. 11 * 12 * Requisitos: 13 * - Carregar o node-forge ANTES deste ficheiro, para existir o global 14 * `forge`. Exemplo via CDN: 15 * <script src="https://cdn.jsdelivr.net/npm/node-forge@1/dist/forge.min.js"></script> 16 * <script src="loginCrypto.js"></script> 17 * - O backend tem de expor GET /auth/publicKey devolvendo a chave pública 18 * RSA em Base64, formato SPKI/X.509 DER, e fazer a decifragem 19 * equivalente do lado do servidor (RSA-OAEP SHA-256 + AES-256-GCM, 20 * concatenando ciphertext+tag tal como o Cipher do Java produz). 21 * 22 * Protocolo: 23 * 1. Pede-se ao backend a chave pública RSA (GET /auth/publicKey). 24 * 2. Gera-se uma chave AES-256-GCM efémera, só para esta cifragem. 25 * 3. Cifra-se a password com essa chave AES (o resultado inclui a tag de 26 * autenticação de 16 bytes no fim). 27 * 4. Cifra-se a própria chave AES com a chave pública RSA (RSA-OAEP, 28 * SHA-256). 29 * 5. Devolve-se { encryptedPassword, encryptedKey, iv }, tudo em Base64, 30 * prontos para enviar ao backend. 31 * 32 * Uso: 33 * LoginCrypto.encryptPassword(password).then(function (result) { 34 * // result.encryptedPassword, result.encryptedKey, result.iv 35 * }); 36 * 37 * Nota: isto é uma camada de defesa em profundidade â não substitui o 38 * HTTPS como proteção do canal de transporte. 39 */ 40 41 42 43(function (global) { 44 'use strict'; 45 46 if (typeof global.forge === 'undefined') { 47 throw new Error( 48 'LoginCrypto: a biblioteca node-forge não foi encontrada. Carregue-a (ex: via CDN) antes deste ficheiro.' 49 ); 50 } 51 var forge = global.forge; 52 53 // Ajustar caso o endpoint da chave pública não esteja na mesma origem 54 // (ex: 'https://outra-api.exemplo.pt/auth/publicKey'). 55 //var PUBLIC_KEY_ENDPOINT = '/web/services/api/webusers/publicKey'; 56 var PUBLIC_KEY_ENDPOINT = 'api/webusers/publicKey'; 57 var cachedPublicKeyPromise = null; 58 59var baseUrlPromises = {}; 60 61 window.handleBaseUrlLoadedCompany = function() { 62 handleBaseUrlLoaded(true); 63}; 64 65window.handleBaseUrlLoadedPerson = function() { 66 handleBaseUrlLoaded(false); 67}; 68 69function extractPath(baseUrl) { 70 var url = new URL(baseUrl); 71 return url; 72} 73 74function getContext(urlTexto) { 75 try { 76 // Cria um objeto URL a partir da string 77 var url = new URL(urlTexto); 78 79 // Extrai o pathname (ex: "/atendimentonet-tq.ano.pt/atendimentonet" ou "/atendimentonet") 80 // e divide pelas barras '/', pegando no último elemento válido 81 var partes = url.pathname.split('/').filter(Boolean); 82 83 return partes[partes.length - 1]; 84 } catch (e) { 85 console.error("URL inválido:", e); 86 return null; 87 } 88} 89 90 91function fetchServicesBaseUrl(isCompany) { 92 var key = isCompany ? "company" : "person"; 93 94 if (!baseUrlPromises[key]) { 95 baseUrlPromises[key] = new Promise(function (resolve, reject) { 96 97 window.handleBaseUrlLoaded = function (isCompany) { 98 99 var holderId = isCompany 100 ? "loginTabs:loginCompanyForm:baseUrlHolder" 101 : "loginTabs:loginPersonForm:baseUrlHolder"; 102 var holder = document.getElementById(holderId); 103 var rawValue = holder ? holder.value : null; 104 105 106 if (!rawValue || !rawValue.trim()) { 107 baseUrlPromises[key] = null; 108 reject(new Error('LoginCrypto: baseUrlHolder vazio.')); 109 return; 110 } 111 112 try { 113 resolve(extractPath(rawValue.trim())); 114 } catch (e) { 115 baseUrlPromises[key] = null; 116 reject(new Error('LoginCrypto: baseUrlHolder não é uma URL válida: ' + rawValue)); 117 } 118 }; 119 120 121 if (isCompany) { 122 getServicesBaseUrlCompany(); 123 } else { 124 getServicesBaseUrlPerson(); 125 } 126 }); 127 } 128 return baseUrlPromises[key]; 129} 130 131 132 /** 133 * Obtém (e mantém em cache) a chave pública RSA do servidor, já
134 * convertida para o formato que o node-forge usa. 135 * @returns {Promise<forge.pki.rsa.PublicKey>} 136 */ 137 function getServerPublicKey(isCompany) { 138 if (!cachedPublicKeyPromise) { 139 cachedPublicKeyPromise = fetchServicesBaseUrl(isCompany) 140 .then(function (path) { 141 return fetch("/" + getContext(path) + "/services/api/futuredoc/publicKey"); 142 }) 143 .then(function (response) { 144 if (!response.ok) { 145 throw new Error( 146 'LoginCrypto: falha ao obter a chave pública (HTTP ' + response.status + ').' 147 ); 148 } 149 return response.json(); 150 }) 151 .then(function (body) { 152 var publicKeyBase64 = 153 (body && body.data && body.data.publicKey) || (body && body.publicKey); 154 155 if (!publicKeyBase64) { 156 throw new Error('LoginCrypto: resposta sem publicKey.'); 157 } 158 var der = forge.util.decode64(publicKeyBase64); 159 var asn1 = forge.asn1.fromDer(der); 160 return forge.pki.publicKeyFromAsn1(asn1); 161 }) 162 .catch(function (err) { 163 cachedPublicKeyPromise = null; 164 throw err; 165 }); 166 } 167 return cachedPublicKeyPromise; 168} 169 170 171 /** 172 * Cifra uma password para envio seguro ao backend. 173 * @param {string} password Password em claro (aplicar trim() antes de chamar). 174 * @returns {Promise<{encryptedPassword: string, encryptedKey: string, iv: string}>} 175 */ 176 function encryptPassword(password,isCompany) { 177 178 return getServerPublicKey(isCompany).then(function (rsaPublicKey) { 179 // Chave AES-256 efémera, gerada de novo em cada cifragem. 180 var aesKeyBytes = forge.random.getBytesSync(32); // 256 bits 181 var ivBytes = forge.random.getBytesSync(12); // 96 bits, padrão para GCM 182 183 var cipher = forge.cipher.createCipher('AES-GCM', aesKeyBytes); 184 cipher.start({ iv: ivBytes }); 185 cipher.update(forge.util.createBuffer(password, 'utf8')); 186 cipher.finish(); 187 188 // O Cipher do Java (AES/GCM/NoPadding) devolve ciphertext + tag 189 // concatenados no doFinal(); replica-se isso aqui para o backend 190 // poder usar exatamente a mesma lógica de decifragem. 191 var ciphertextWithTag = cipher.output.getBytes() + cipher.mode.tag.getBytes(); 192 193 var wrappedKey = rsaPublicKey.encrypt(aesKeyBytes, 'RSA-OAEP', { 194 md: forge.md.sha256.create(), 195 mgf1: { md: forge.md.sha256.create() }, 196 }); 197 198 return { 199 encryptedPassword: forge.util.encode64(ciphertextWithTag), 200 encryptedKey: forge.util.encode64(wrappedKey), 201 iv: forge.util.encode64(ivBytes), 202 }; 203 }); 204 } 205 206 global.LoginCrypto = { 207 encryptPassword: encryptPassword, 208 }; 209})(typeof window !== 'undefined' ? window : this); 210
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.