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)