github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/assets/scripts/password-helpers.js (about) 1 ;(function (w) { 2 // Return given password strength as an object {percentage, label} 3 function getStrength(password) { 4 if (!password && password !== '') { 5 throw new Error('password parameter is missing') 6 } 7 if (!password.length) { 8 return { percentage: 0, label: 'weak' } 9 } 10 11 const charsets = [ 12 // upper 13 { regexp: /\p{Lu}/u, size: 26 }, 14 // lower 15 { regexp: /\p{Ll}/u, size: 26 }, 16 // digit 17 { regexp: /[0-9]/, size: 10 }, 18 // special 19 { regexp: /\p{P}|\p{S}/u, size: 30 }, 20 ] 21 22 const possibleChars = charsets.reduce(function (possibleChars, charset) { 23 let chars = possibleChars 24 if (charset.regexp.test(password)) chars += charset.size 25 return chars 26 }, 0) 27 28 const passwordStrength = 29 Math.log(Math.pow(possibleChars, password.length)) / Math.log(2) 30 31 // levels 32 const _at33percent = 50 33 const _at66percent = 100 34 const _at100percent = 150 35 36 let strengthLabel = '' 37 let strengthPercentage = 0 38 39 // between 0% and 33% 40 if (passwordStrength <= _at33percent) { 41 strengthPercentage = (passwordStrength * 33) / _at33percent 42 strengthLabel = 'weak' 43 } else if ( 44 passwordStrength > _at33percent && 45 passwordStrength <= _at66percent 46 ) { 47 // between 33% and 66% 48 strengthPercentage = (passwordStrength * 66) / _at66percent 49 strengthLabel = 'moderate' 50 } else { 51 // passwordStrength > 192 52 strengthPercentage = (passwordStrength * 100) / _at100percent 53 if (strengthPercentage > 100) strengthPercentage = 100 54 strengthLabel = 'strong' 55 } 56 57 return { percentage: strengthPercentage, label: strengthLabel } 58 } 59 60 function fromUtf8ToArray(str) { 61 const strUtf8 = unescape(encodeURIComponent(str)) 62 const arr = new Uint8Array(strUtf8.length) 63 for (let i = 0; i < strUtf8.length; i++) { 64 arr[i] = strUtf8.charCodeAt(i) 65 } 66 return arr 67 } 68 69 // Return a promise that resolves to the hash of the master password. 70 // This implementation uses the asmcrypto.js lib (for Edge support). 71 function jsHash(password, salt, iterations) { 72 // 256 bits of sha-256 can be saved in a Uint8Array of length 32 73 const length = 32 74 const pbkdf2 = w.asmCrypto.Pbkdf2HmacSha256 75 const passwordArr = fromUtf8ToArray(password) 76 const saltArr = fromUtf8ToArray(salt) 77 const master = pbkdf2(passwordArr, saltArr, iterations, length) 78 const hashed = pbkdf2(master, passwordArr, 1, length) 79 let binary = '' 80 for (let i = 0; i < hashed.byteLength; i++) { 81 binary += String.fromCharCode(hashed[i]) 82 } 83 return Promise.resolve({ 84 hashed: w.btoa(binary), 85 masterKey: master.buffer, 86 }) 87 } 88 89 // Return a promise that resolves to the hash of the master password. 90 // This implementation uses the native crypto.subtle from the browser. 91 function nativeHash(password, salt, iterations) { 92 const subtle = w.crypto.subtle 93 const passwordBuf = fromUtf8ToArray(password).buffer 94 const saltBuf = fromUtf8ToArray(salt).buffer 95 const first = { 96 name: 'PBKDF2', 97 salt: saltBuf, 98 iterations: iterations, 99 hash: { name: 'SHA-256' }, 100 } 101 const second = { 102 name: 'PBKDF2', 103 salt: passwordBuf, 104 iterations: 1, 105 hash: { name: 'SHA-256' }, 106 } 107 let masterKey 108 return subtle 109 .importKey('raw', passwordBuf, { name: 'PBKDF2' }, false, ['deriveBits']) 110 .then((material) => subtle.deriveBits(first, material, 256)) 111 .then((key) => { 112 masterKey = key 113 return subtle.importKey('raw', key, { name: 'PBKDF2' }, false, [ 114 'deriveBits', 115 ]) 116 }) 117 .then((material) => subtle.deriveBits(second, material, 256)) 118 .then((hashed) => { 119 let binary = '' 120 const bytes = new Uint8Array(hashed) 121 for (let i = 0; i < bytes.byteLength; i++) { 122 binary += String.fromCharCode(bytes[i]) 123 } 124 return { hashed: w.btoa(binary), masterKey: masterKey } 125 }) 126 } 127 128 function randomBytes(length) { 129 const arr = new Uint8Array(length) 130 w.crypto.getRandomValues(arr) 131 return arr.buffer 132 } 133 134 function fromBufferToB64(buffer) { 135 let binary = '' 136 const bytes = new Uint8Array(buffer) 137 for (let i = 0; i < bytes.byteLength; i++) { 138 binary += String.fromCharCode(bytes[i]) 139 } 140 return w.btoa(binary) 141 } 142 143 // Returns a promise that resolves to a new encryption key, encrypted with 144 // the masterKey and ready to be sent to the server on onboarding. 145 function makeEncKey(masterKey) { 146 const subtle = w.crypto.subtle 147 const encKey = randomBytes(64) 148 const iv = randomBytes(16) 149 return subtle 150 .importKey('raw', masterKey, { name: 'AES-CBC' }, false, ['encrypt']) 151 .then((impKey) => 152 subtle.encrypt({ name: 'AES-CBC', iv: iv }, impKey, encKey), 153 ) 154 .then((encrypted) => { 155 const iv64 = fromBufferToB64(iv) 156 const data = fromBufferToB64(encrypted) 157 return { 158 // 0 means AesCbc256_B64 159 cipherString: `0.${iv64}|${data}`, 160 key: encKey, 161 } 162 }) 163 } 164 165 // Returns a promise that resolves to a new key pair, with the private key 166 // encrypted with the encryption key, and the public key encoded in base64. 167 function makeKeyPair(symKey) { 168 const subtle = w.crypto.subtle 169 const encKey = symKey.slice(0, 32) 170 const macKey = symKey.slice(32, 64) 171 const iv = randomBytes(16) 172 const rsaParams = { 173 name: 'RSA-OAEP', 174 modulusLength: 2048, 175 publicExponent: new Uint8Array([0x01, 0x00, 0x01]), // 65537 176 hash: { name: 'SHA-1' }, 177 } 178 const hmacParams = { name: 'HMAC', hash: 'SHA-256' } 179 let publicKey, privateKey, encryptedKey 180 return subtle 181 .generateKey(rsaParams, true, ['encrypt', 'decrypt']) 182 .then((pair) => { 183 const publicPromise = subtle.exportKey('spki', pair.publicKey) 184 const privatePromise = subtle.exportKey('pkcs8', pair.privateKey) 185 return Promise.all([publicPromise, privatePromise]) 186 }) 187 .then((keys) => { 188 publicKey = keys[0] 189 privateKey = keys[1] 190 return subtle.importKey('raw', encKey, { name: 'AES-CBC' }, false, [ 191 'encrypt', 192 ]) 193 }) 194 .then((impKey) => 195 subtle.encrypt({ name: 'AES-CBC', iv: iv }, impKey, privateKey), 196 ) 197 .then((encrypted) => { 198 encryptedKey = encrypted 199 return subtle.importKey('raw', macKey, hmacParams, false, ['sign']) 200 }) 201 .then((impKey) => { 202 const macData = new Uint8Array(iv.byteLength + encryptedKey.byteLength) 203 macData.set(new Uint8Array(iv), 0) 204 macData.set(new Uint8Array(encryptedKey), iv.byteLength) 205 return subtle.sign(hmacParams, impKey, macData) 206 }) 207 .then((mac) => { 208 const public64 = fromBufferToB64(publicKey) 209 const iv64 = fromBufferToB64(iv) 210 const priv = fromBufferToB64(encryptedKey) 211 const mac64 = fromBufferToB64(mac) 212 return { 213 publicKey: public64, 214 // 2 means AesCbc256_HmacSha256_B64 215 privateKey: `2.${iv64}|${priv}|${mac64}`, 216 } 217 }) 218 } 219 220 const hash = w.asmCrypto ? jsHash : nativeHash 221 222 w.password = { 223 getStrength: getStrength, 224 hash: hash, 225 makeEncKey: makeEncKey, 226 makeKeyPair: makeKeyPair, 227 } 228 })(window)