File indexing completed on 2024-05-19 06:01:40

0001 /*
0002 * Fingerprintjs2 2.1.0 - Modern & flexible browser fingerprint library v2
0003 * https://github.com/Valve/fingerprintjs2
0004 * Copyright (c) 2015 Valentin Vasilyev (valentin.vasilyev@outlook.com)
0005 * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
0006 *
0007 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
0008 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
0009 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
0010 * ARE DISCLAIMED. IN NO EVENT SHALL VALENTIN VASILYEV BE LIABLE FOR ANY
0011 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
0012 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
0013 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
0014 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
0015 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
0016 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
0017 */
0018 /* global define */
0019 (function (name, context, definition) {
0020   'use strict'
0021   if (typeof window !== 'undefined' && typeof define === 'function' && define.amd) { define(definition) } else if (typeof module !== 'undefined' && module.exports) { module.exports = definition() } else if (context.exports) { context.exports = definition() } else { context[name] = definition() }
0022 })('Fingerprint2', this, function () {
0023   'use strict'
0024 
0025   /// MurmurHash3 related functions
0026 
0027   //
0028   // Given two 64bit ints (as an array of two 32bit ints) returns the two
0029   // added together as a 64bit int (as an array of two 32bit ints).
0030   //
0031   var x64Add = function (m, n) {
0032     m = [m[0] >>> 16, m[0] & 0xffff, m[1] >>> 16, m[1] & 0xffff]
0033     n = [n[0] >>> 16, n[0] & 0xffff, n[1] >>> 16, n[1] & 0xffff]
0034     var o = [0, 0, 0, 0]
0035     o[3] += m[3] + n[3]
0036     o[2] += o[3] >>> 16
0037     o[3] &= 0xffff
0038     o[2] += m[2] + n[2]
0039     o[1] += o[2] >>> 16
0040     o[2] &= 0xffff
0041     o[1] += m[1] + n[1]
0042     o[0] += o[1] >>> 16
0043     o[1] &= 0xffff
0044     o[0] += m[0] + n[0]
0045     o[0] &= 0xffff
0046     return [(o[0] << 16) | o[1], (o[2] << 16) | o[3]]
0047   }
0048 
0049   //
0050   // Given two 64bit ints (as an array of two 32bit ints) returns the two
0051   // multiplied together as a 64bit int (as an array of two 32bit ints).
0052   //
0053   var x64Multiply = function (m, n) {
0054     m = [m[0] >>> 16, m[0] & 0xffff, m[1] >>> 16, m[1] & 0xffff]
0055     n = [n[0] >>> 16, n[0] & 0xffff, n[1] >>> 16, n[1] & 0xffff]
0056     var o = [0, 0, 0, 0]
0057     o[3] += m[3] * n[3]
0058     o[2] += o[3] >>> 16
0059     o[3] &= 0xffff
0060     o[2] += m[2] * n[3]
0061     o[1] += o[2] >>> 16
0062     o[2] &= 0xffff
0063     o[2] += m[3] * n[2]
0064     o[1] += o[2] >>> 16
0065     o[2] &= 0xffff
0066     o[1] += m[1] * n[3]
0067     o[0] += o[1] >>> 16
0068     o[1] &= 0xffff
0069     o[1] += m[2] * n[2]
0070     o[0] += o[1] >>> 16
0071     o[1] &= 0xffff
0072     o[1] += m[3] * n[1]
0073     o[0] += o[1] >>> 16
0074     o[1] &= 0xffff
0075     o[0] += (m[0] * n[3]) + (m[1] * n[2]) + (m[2] * n[1]) + (m[3] * n[0])
0076     o[0] &= 0xffff
0077     return [(o[0] << 16) | o[1], (o[2] << 16) | o[3]]
0078   }
0079   //
0080   // Given a 64bit int (as an array of two 32bit ints) and an int
0081   // representing a number of bit positions, returns the 64bit int (as an
0082   // array of two 32bit ints) rotated left by that number of positions.
0083   //
0084   var x64Rotl = function (m, n) {
0085     n %= 64
0086     if (n === 32) {
0087       return [m[1], m[0]]
0088     } else if (n < 32) {
0089       return [(m[0] << n) | (m[1] >>> (32 - n)), (m[1] << n) | (m[0] >>> (32 - n))]
0090     } else {
0091       n -= 32
0092       return [(m[1] << n) | (m[0] >>> (32 - n)), (m[0] << n) | (m[1] >>> (32 - n))]
0093     }
0094   }
0095   //
0096   // Given a 64bit int (as an array of two 32bit ints) and an int
0097   // representing a number of bit positions, returns the 64bit int (as an
0098   // array of two 32bit ints) shifted left by that number of positions.
0099   //
0100   var x64LeftShift = function (m, n) {
0101     n %= 64
0102     if (n === 0) {
0103       return m
0104     } else if (n < 32) {
0105       return [(m[0] << n) | (m[1] >>> (32 - n)), m[1] << n]
0106     } else {
0107       return [m[1] << (n - 32), 0]
0108     }
0109   }
0110   //
0111   // Given two 64bit ints (as an array of two 32bit ints) returns the two
0112   // xored together as a 64bit int (as an array of two 32bit ints).
0113   //
0114   var x64Xor = function (m, n) {
0115     return [m[0] ^ n[0], m[1] ^ n[1]]
0116   }
0117   //
0118   // Given a block, returns murmurHash3's final x64 mix of that block.
0119   // (`[0, h[0] >>> 1]` is a 33 bit unsigned right shift. This is the
0120   // only place where we need to right shift 64bit ints.)
0121   //
0122   var x64Fmix = function (h) {
0123     h = x64Xor(h, [0, h[0] >>> 1])
0124     h = x64Multiply(h, [0xff51afd7, 0xed558ccd])
0125     h = x64Xor(h, [0, h[0] >>> 1])
0126     h = x64Multiply(h, [0xc4ceb9fe, 0x1a85ec53])
0127     h = x64Xor(h, [0, h[0] >>> 1])
0128     return h
0129   }
0130 
0131   //
0132   // Given a string and an optional seed as an int, returns a 128 bit
0133   // hash using the x64 flavor of MurmurHash3, as an unsigned hex.
0134   //
0135   var x64hash128 = function (key, seed) {
0136     key = key || ''
0137     seed = seed || 0
0138     var remainder = key.length % 16
0139     var bytes = key.length - remainder
0140     var h1 = [0, seed]
0141     var h2 = [0, seed]
0142     var k1 = [0, 0]
0143     var k2 = [0, 0]
0144     var c1 = [0x87c37b91, 0x114253d5]
0145     var c2 = [0x4cf5ad43, 0x2745937f]
0146     for (var i = 0; i < bytes; i = i + 16) {
0147       k1 = [((key.charCodeAt(i + 4) & 0xff)) | ((key.charCodeAt(i + 5) & 0xff) << 8) | ((key.charCodeAt(i + 6) & 0xff) << 16) | ((key.charCodeAt(i + 7) & 0xff) << 24), ((key.charCodeAt(i) & 0xff)) | ((key.charCodeAt(i + 1) & 0xff) << 8) | ((key.charCodeAt(i + 2) & 0xff) << 16) | ((key.charCodeAt(i + 3) & 0xff) << 24)]
0148       k2 = [((key.charCodeAt(i + 12) & 0xff)) | ((key.charCodeAt(i + 13) & 0xff) << 8) | ((key.charCodeAt(i + 14) & 0xff) << 16) | ((key.charCodeAt(i + 15) & 0xff) << 24), ((key.charCodeAt(i + 8) & 0xff)) | ((key.charCodeAt(i + 9) & 0xff) << 8) | ((key.charCodeAt(i + 10) & 0xff) << 16) | ((key.charCodeAt(i + 11) & 0xff) << 24)]
0149       k1 = x64Multiply(k1, c1)
0150       k1 = x64Rotl(k1, 31)
0151       k1 = x64Multiply(k1, c2)
0152       h1 = x64Xor(h1, k1)
0153       h1 = x64Rotl(h1, 27)
0154       h1 = x64Add(h1, h2)
0155       h1 = x64Add(x64Multiply(h1, [0, 5]), [0, 0x52dce729])
0156       k2 = x64Multiply(k2, c2)
0157       k2 = x64Rotl(k2, 33)
0158       k2 = x64Multiply(k2, c1)
0159       h2 = x64Xor(h2, k2)
0160       h2 = x64Rotl(h2, 31)
0161       h2 = x64Add(h2, h1)
0162       h2 = x64Add(x64Multiply(h2, [0, 5]), [0, 0x38495ab5])
0163     }
0164     k1 = [0, 0]
0165     k2 = [0, 0]
0166     switch (remainder) {
0167       case 15:
0168         k2 = x64Xor(k2, x64LeftShift([0, key.charCodeAt(i + 14)], 48))
0169       // fallthrough
0170       case 14:
0171         k2 = x64Xor(k2, x64LeftShift([0, key.charCodeAt(i + 13)], 40))
0172       // fallthrough
0173       case 13:
0174         k2 = x64Xor(k2, x64LeftShift([0, key.charCodeAt(i + 12)], 32))
0175       // fallthrough
0176       case 12:
0177         k2 = x64Xor(k2, x64LeftShift([0, key.charCodeAt(i + 11)], 24))
0178       // fallthrough
0179       case 11:
0180         k2 = x64Xor(k2, x64LeftShift([0, key.charCodeAt(i + 10)], 16))
0181       // fallthrough
0182       case 10:
0183         k2 = x64Xor(k2, x64LeftShift([0, key.charCodeAt(i + 9)], 8))
0184       // fallthrough
0185       case 9:
0186         k2 = x64Xor(k2, [0, key.charCodeAt(i + 8)])
0187         k2 = x64Multiply(k2, c2)
0188         k2 = x64Rotl(k2, 33)
0189         k2 = x64Multiply(k2, c1)
0190         h2 = x64Xor(h2, k2)
0191       // fallthrough
0192       case 8:
0193         k1 = x64Xor(k1, x64LeftShift([0, key.charCodeAt(i + 7)], 56))
0194       // fallthrough
0195       case 7:
0196         k1 = x64Xor(k1, x64LeftShift([0, key.charCodeAt(i + 6)], 48))
0197       // fallthrough
0198       case 6:
0199         k1 = x64Xor(k1, x64LeftShift([0, key.charCodeAt(i + 5)], 40))
0200       // fallthrough
0201       case 5:
0202         k1 = x64Xor(k1, x64LeftShift([0, key.charCodeAt(i + 4)], 32))
0203       // fallthrough
0204       case 4:
0205         k1 = x64Xor(k1, x64LeftShift([0, key.charCodeAt(i + 3)], 24))
0206       // fallthrough
0207       case 3:
0208         k1 = x64Xor(k1, x64LeftShift([0, key.charCodeAt(i + 2)], 16))
0209       // fallthrough
0210       case 2:
0211         k1 = x64Xor(k1, x64LeftShift([0, key.charCodeAt(i + 1)], 8))
0212       // fallthrough
0213       case 1:
0214         k1 = x64Xor(k1, [0, key.charCodeAt(i)])
0215         k1 = x64Multiply(k1, c1)
0216         k1 = x64Rotl(k1, 31)
0217         k1 = x64Multiply(k1, c2)
0218         h1 = x64Xor(h1, k1)
0219       // fallthrough
0220     }
0221     h1 = x64Xor(h1, [0, key.length])
0222     h2 = x64Xor(h2, [0, key.length])
0223     h1 = x64Add(h1, h2)
0224     h2 = x64Add(h2, h1)
0225     h1 = x64Fmix(h1)
0226     h2 = x64Fmix(h2)
0227     h1 = x64Add(h1, h2)
0228     h2 = x64Add(h2, h1)
0229     return ('00000000' + (h1[0] >>> 0).toString(16)).slice(-8) + ('00000000' + (h1[1] >>> 0).toString(16)).slice(-8) + ('00000000' + (h2[0] >>> 0).toString(16)).slice(-8) + ('00000000' + (h2[1] >>> 0).toString(16)).slice(-8)
0230   }
0231 
0232   var defaultOptions = {
0233     preprocessor: null,
0234     audio: {
0235       timeout: 1000,
0236       // On iOS 11, audio context can only be used in response to user interaction.
0237       // We require users to explicitly enable audio fingerprinting on iOS 11.
0238       // See https://stackoverflow.com/questions/46363048/onaudioprocess-not-called-on-ios11#46534088
0239       excludeIOS11: true
0240     },
0241     fonts: {
0242       swfContainerId: 'fingerprintjs2',
0243       swfPath: 'flash/compiled/FontList.swf',
0244       userDefinedFonts: [],
0245       extendedJsFonts: false
0246     },
0247     screen: {
0248       // To ensure consistent fingerprints when users rotate their mobile devices
0249       detectScreenOrientation: true
0250     },
0251     plugins: {
0252       sortPluginsFor: [/palemoon/i],
0253       excludeIE: false
0254     },
0255     extraComponents: [],
0256     excludes: {
0257       // Unreliable on Windows, see https://github.com/Valve/fingerprintjs2/issues/375
0258       'enumerateDevices': true,
0259       // devicePixelRatio depends on browser zoom, and it's impossible to detect browser zoom
0260       'pixelRatio': true,
0261       // DNT depends on incognito mode for some browsers (Chrome) and it's impossible to detect incognito mode
0262       'doNotTrack': true,
0263       // uses js fonts already
0264       'fontsFlash': true
0265     },
0266     NOT_AVAILABLE: 'not available',
0267     ERROR: 'error',
0268     EXCLUDED: 'excluded'
0269   }
0270 
0271   var each = function (obj, iterator) {
0272     if (Array.prototype.forEach && obj.forEach === Array.prototype.forEach) {
0273       obj.forEach(iterator)
0274     } else if (obj.length === +obj.length) {
0275       for (var i = 0, l = obj.length; i < l; i++) {
0276         iterator(obj[i], i, obj)
0277       }
0278     } else {
0279       for (var key in obj) {
0280         if (obj.hasOwnProperty(key)) {
0281           iterator(obj[key], key, obj)
0282         }
0283       }
0284     }
0285   }
0286 
0287   var map = function (obj, iterator) {
0288     var results = []
0289     // Not using strict equality so that this acts as a
0290     // shortcut to checking for `null` and `undefined`.
0291     if (obj == null) {
0292       return results
0293     }
0294     if (Array.prototype.map && obj.map === Array.prototype.map) { return obj.map(iterator) }
0295     each(obj, function (value, index, list) {
0296       results.push(iterator(value, index, list))
0297     })
0298     return results
0299   }
0300 
0301   var extendSoft = function (target, source) {
0302     if (source == null) { return target }
0303     var value
0304     var key
0305     for (key in source) {
0306       value = source[key]
0307       if (value != null && !(Object.prototype.hasOwnProperty.call(target, key))) {
0308         target[key] = value
0309       }
0310     }
0311     return target
0312   }
0313 
0314   // https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/enumerateDevices
0315   var enumerateDevicesKey = function (done, options) {
0316     if (!isEnumerateDevicesSupported()) {
0317       return done(options.NOT_AVAILABLE)
0318     }
0319     navigator.mediaDevices.enumerateDevices().then(function (devices) {
0320       done(devices.map(function (device) {
0321         return 'id=' + device.deviceId + ';gid=' + device.groupId + ';' + device.kind + ';' + device.label
0322       }))
0323     })
0324       .catch(function (error) {
0325         done(error)
0326       })
0327   }
0328 
0329   var isEnumerateDevicesSupported = function () {
0330     return (navigator.mediaDevices && navigator.mediaDevices.enumerateDevices)
0331   }
0332   // Inspired by and based on https://github.com/cozylife/audio-fingerprint
0333   var audioKey = function (done, options) {
0334     var audioOptions = options.audio
0335     if (audioOptions.excludeIOS11 && navigator.userAgent.match(/OS 11.+Version\/11.+Safari/)) {
0336       // See comment for excludeUserAgent and https://stackoverflow.com/questions/46363048/onaudioprocess-not-called-on-ios11#46534088
0337       return done(options.EXCLUDED)
0338     }
0339 
0340     var AudioContext = window.OfflineAudioContext || window.webkitOfflineAudioContext
0341 
0342     if (AudioContext == null) {
0343       return done(options.NOT_AVAILABLE)
0344     }
0345 
0346     var context = new AudioContext(1, 44100, 44100)
0347 
0348     var oscillator = context.createOscillator()
0349     oscillator.type = 'triangle'
0350     oscillator.frequency.setValueAtTime(10000, context.currentTime)
0351 
0352     var compressor = context.createDynamicsCompressor()
0353     each([
0354       ['threshold', -50],
0355       ['knee', 40],
0356       ['ratio', 12],
0357       ['reduction', -20],
0358       ['attack', 0],
0359       ['release', 0.25]
0360     ], function (item) {
0361       if (compressor[item[0]] !== undefined && typeof compressor[item[0]].setValueAtTime === 'function') {
0362         compressor[item[0]].setValueAtTime(item[1], context.currentTime)
0363       }
0364     })
0365 
0366     oscillator.connect(compressor)
0367     compressor.connect(context.destination)
0368     oscillator.start(0)
0369     context.startRendering()
0370 
0371     var audioTimeoutId = setTimeout(function () {
0372       console.warn('Audio fingerprint timed out. Please report bug at https://github.com/Valve/fingerprintjs2 with your user agent: "' + navigator.userAgent + '".')
0373       context.oncomplete = function () { }
0374       context = null
0375       return done('audioTimeout')
0376     }, audioOptions.timeout)
0377 
0378     context.oncomplete = function (event) {
0379       var fingerprint
0380       try {
0381         clearTimeout(audioTimeoutId)
0382         fingerprint = event.renderedBuffer.getChannelData(0)
0383           .slice(4500, 5000)
0384           .reduce(function (acc, val) { return acc + Math.abs(val) }, 0)
0385           .toString()
0386         oscillator.disconnect()
0387         compressor.disconnect()
0388       } catch (error) {
0389         done(error)
0390         return
0391       }
0392       done(fingerprint)
0393     }
0394   }
0395   var UserAgent = function (done) {
0396     done(navigator.userAgent)
0397   }
0398   var webdriver = function (done, options) {
0399     done(navigator.webdriver == null ? options.NOT_AVAILABLE : navigator.webdriver)
0400   }
0401   var languageKey = function (done, options) {
0402     done(navigator.language || navigator.userLanguage || navigator.browserLanguage || navigator.systemLanguage || options.NOT_AVAILABLE)
0403   }
0404   var colorDepthKey = function (done, options) {
0405     done(window.screen.colorDepth || options.NOT_AVAILABLE)
0406   }
0407   var deviceMemoryKey = function (done, options) {
0408     done(navigator.deviceMemory || options.NOT_AVAILABLE)
0409   }
0410   var pixelRatioKey = function (done, options) {
0411     done(window.devicePixelRatio || options.NOT_AVAILABLE)
0412   }
0413   var screenResolutionKey = function (done, options) {
0414     done(getScreenResolution(options))
0415   }
0416   var getScreenResolution = function (options) {
0417     var resolution = [window.screen.width, window.screen.height]
0418     if (options.screen.detectScreenOrientation) {
0419       resolution.sort().reverse()
0420     }
0421     return resolution
0422   }
0423   var availableScreenResolutionKey = function (done, options) {
0424     done(getAvailableScreenResolution(options))
0425   }
0426   var getAvailableScreenResolution = function (options) {
0427     if (window.screen.availWidth && window.screen.availHeight) {
0428       var available = [window.screen.availHeight, window.screen.availWidth]
0429       if (options.screen.detectScreenOrientation) {
0430         available.sort().reverse()
0431       }
0432       return available
0433     }
0434     // headless browsers
0435     return options.NOT_AVAILABLE
0436   }
0437   var timezoneOffset = function (done) {
0438     done(new Date().getTimezoneOffset())
0439   }
0440   var timezone = function (done, options) {
0441     if (window.Intl && window.Intl.DateTimeFormat) {
0442       done(new window.Intl.DateTimeFormat().resolvedOptions().timeZone)
0443       return
0444     }
0445     done(options.NOT_AVAILABLE)
0446   }
0447   var sessionStorageKey = function (done, options) {
0448     done(hasSessionStorage(options))
0449   }
0450   var localStorageKey = function (done, options) {
0451     done(hasLocalStorage(options))
0452   }
0453   var indexedDbKey = function (done, options) {
0454     done(hasIndexedDB(options))
0455   }
0456   var addBehaviorKey = function (done) {
0457     // body might not be defined at this point or removed programmatically
0458     done(!!(document.body && document.body.addBehavior))
0459   }
0460   var openDatabaseKey = function (done) {
0461     done(!!window.openDatabase)
0462   }
0463   var cpuClassKey = function (done, options) {
0464     done(getNavigatorCpuClass(options))
0465   }
0466   var platformKey = function (done, options) {
0467     done(getNavigatorPlatform(options))
0468   }
0469   var doNotTrackKey = function (done, options) {
0470     done(getDoNotTrack(options))
0471   }
0472   var canvasKey = function (done, options) {
0473     if (isCanvasSupported()) {
0474       done(getCanvasFp(options))
0475       return
0476     }
0477     done(options.NOT_AVAILABLE)
0478   }
0479   var webglKey = function (done, options) {
0480     if (isWebGlSupported()) {
0481       done(getWebglFp())
0482       return
0483     }
0484     done(options.NOT_AVAILABLE)
0485   }
0486   var webglVendorAndRendererKey = function (done) {
0487     if (isWebGlSupported()) {
0488       done(getWebglVendorAndRenderer())
0489       return
0490     }
0491     done()
0492   }
0493   var adBlockKey = function (done) {
0494     done(getAdBlock())
0495   }
0496   var hasLiedLanguagesKey = function (done) {
0497     done(getHasLiedLanguages())
0498   }
0499   var hasLiedResolutionKey = function (done) {
0500     done(getHasLiedResolution())
0501   }
0502   var hasLiedOsKey = function (done) {
0503     done(getHasLiedOs())
0504   }
0505   var hasLiedBrowserKey = function (done) {
0506     done(getHasLiedBrowser())
0507   }
0508   // flash fonts (will increase fingerprinting time 20X to ~ 130-150ms)
0509   var flashFontsKey = function (done, options) {
0510     // we do flash if swfobject is loaded
0511     if (!hasSwfObjectLoaded()) {
0512       return done('swf object not loaded')
0513     }
0514     if (!hasMinFlashInstalled()) {
0515       return done('flash not installed')
0516     }
0517     if (!options.fonts.swfPath) {
0518       return done('missing options.fonts.swfPath')
0519     }
0520     loadSwfAndDetectFonts(function (fonts) {
0521       done(fonts)
0522     }, options)
0523   }
0524   // kudos to http://www.lalit.org/lab/javascript-css-font-detect/
0525   var jsFontsKey = function (done, options) {
0526     // a font will be compared against all the three default fonts.
0527     // and if it doesn't match all 3 then that font is not available.
0528     var baseFonts = ['monospace', 'sans-serif', 'serif']
0529 
0530     var fontList = [
0531       'Andale Mono', 'Arial', 'Arial Black', 'Arial Hebrew', 'Arial MT', 'Arial Narrow', 'Arial Rounded MT Bold', 'Arial Unicode MS',
0532       'Bitstream Vera Sans Mono', 'Book Antiqua', 'Bookman Old Style',
0533       'Calibri', 'Cambria', 'Cambria Math', 'Century', 'Century Gothic', 'Century Schoolbook', 'Comic Sans', 'Comic Sans MS', 'Consolas', 'Courier', 'Courier New',
0534       'Geneva', 'Georgia',
0535       'Helvetica', 'Helvetica Neue',
0536       'Impact',
0537       'Lucida Bright', 'Lucida Calligraphy', 'Lucida Console', 'Lucida Fax', 'LUCIDA GRANDE', 'Lucida Handwriting', 'Lucida Sans', 'Lucida Sans Typewriter', 'Lucida Sans Unicode',
0538       'Microsoft Sans Serif', 'Monaco', 'Monotype Corsiva', 'MS Gothic', 'MS Outlook', 'MS PGothic', 'MS Reference Sans Serif', 'MS Sans Serif', 'MS Serif', 'MYRIAD', 'MYRIAD PRO',
0539       'Palatino', 'Palatino Linotype',
0540       'Segoe Print', 'Segoe Script', 'Segoe UI', 'Segoe UI Light', 'Segoe UI Semibold', 'Segoe UI Symbol',
0541       'Tahoma', 'Times', 'Times New Roman', 'Times New Roman PS', 'Trebuchet MS',
0542       'Verdana', 'Wingdings', 'Wingdings 2', 'Wingdings 3'
0543     ]
0544 
0545     if (options.fonts.extendedJsFonts) {
0546       var extendedFontList = [
0547         'Abadi MT Condensed Light', 'Academy Engraved LET', 'ADOBE CASLON PRO', 'Adobe Garamond', 'ADOBE GARAMOND PRO', 'Agency FB', 'Aharoni', 'Albertus Extra Bold', 'Albertus Medium', 'Algerian', 'Amazone BT', 'American Typewriter',
0548         'American Typewriter Condensed', 'AmerType Md BT', 'Andalus', 'Angsana New', 'AngsanaUPC', 'Antique Olive', 'Aparajita', 'Apple Chancery', 'Apple Color Emoji', 'Apple SD Gothic Neo', 'Arabic Typesetting', 'ARCHER',
0549         'ARNO PRO', 'Arrus BT', 'Aurora Cn BT', 'AvantGarde Bk BT', 'AvantGarde Md BT', 'AVENIR', 'Ayuthaya', 'Bandy', 'Bangla Sangam MN', 'Bank Gothic', 'BankGothic Md BT', 'Baskerville',
0550         'Baskerville Old Face', 'Batang', 'BatangChe', 'Bauer Bodoni', 'Bauhaus 93', 'Bazooka', 'Bell MT', 'Bembo', 'Benguiat Bk BT', 'Berlin Sans FB', 'Berlin Sans FB Demi', 'Bernard MT Condensed', 'BernhardFashion BT', 'BernhardMod BT', 'Big Caslon', 'BinnerD',
0551         'Blackadder ITC', 'BlairMdITC TT', 'Bodoni 72', 'Bodoni 72 Oldstyle', 'Bodoni 72 Smallcaps', 'Bodoni MT', 'Bodoni MT Black', 'Bodoni MT Condensed', 'Bodoni MT Poster Compressed',
0552         'Bookshelf Symbol 7', 'Boulder', 'Bradley Hand', 'Bradley Hand ITC', 'Bremen Bd BT', 'Britannic Bold', 'Broadway', 'Browallia New', 'BrowalliaUPC', 'Brush Script MT', 'Californian FB', 'Calisto MT', 'Calligrapher', 'Candara',
0553         'CaslonOpnface BT', 'Castellar', 'Centaur', 'Cezanne', 'CG Omega', 'CG Times', 'Chalkboard', 'Chalkboard SE', 'Chalkduster', 'Charlesworth', 'Charter Bd BT', 'Charter BT', 'Chaucer',
0554         'ChelthmITC Bk BT', 'Chiller', 'Clarendon', 'Clarendon Condensed', 'CloisterBlack BT', 'Cochin', 'Colonna MT', 'Constantia', 'Cooper Black', 'Copperplate', 'Copperplate Gothic', 'Copperplate Gothic Bold',
0555         'Copperplate Gothic Light', 'CopperplGoth Bd BT', 'Corbel', 'Cordia New', 'CordiaUPC', 'Cornerstone', 'Coronet', 'Cuckoo', 'Curlz MT', 'DaunPenh', 'Dauphin', 'David', 'DB LCD Temp', 'DELICIOUS', 'Denmark',
0556         'DFKai-SB', 'Didot', 'DilleniaUPC', 'DIN', 'DokChampa', 'Dotum', 'DotumChe', 'Ebrima', 'Edwardian Script ITC', 'Elephant', 'English 111 Vivace BT', 'Engravers MT', 'EngraversGothic BT', 'Eras Bold ITC', 'Eras Demi ITC', 'Eras Light ITC', 'Eras Medium ITC',
0557         'EucrosiaUPC', 'Euphemia', 'Euphemia UCAS', 'EUROSTILE', 'Exotc350 Bd BT', 'FangSong', 'Felix Titling', 'Fixedsys', 'FONTIN', 'Footlight MT Light', 'Forte',
0558         'FrankRuehl', 'Fransiscan', 'Freefrm721 Blk BT', 'FreesiaUPC', 'Freestyle Script', 'French Script MT', 'FrnkGothITC Bk BT', 'Fruitger', 'FRUTIGER',
0559         'Futura', 'Futura Bk BT', 'Futura Lt BT', 'Futura Md BT', 'Futura ZBlk BT', 'FuturaBlack BT', 'Gabriola', 'Galliard BT', 'Gautami', 'Geeza Pro', 'Geometr231 BT', 'Geometr231 Hv BT', 'Geometr231 Lt BT', 'GeoSlab 703 Lt BT',
0560         'GeoSlab 703 XBd BT', 'Gigi', 'Gill Sans', 'Gill Sans MT', 'Gill Sans MT Condensed', 'Gill Sans MT Ext Condensed Bold', 'Gill Sans Ultra Bold', 'Gill Sans Ultra Bold Condensed', 'Gisha', 'Gloucester MT Extra Condensed', 'GOTHAM', 'GOTHAM BOLD',
0561         'Goudy Old Style', 'Goudy Stout', 'GoudyHandtooled BT', 'GoudyOLSt BT', 'Gujarati Sangam MN', 'Gulim', 'GulimChe', 'Gungsuh', 'GungsuhChe', 'Gurmukhi MN', 'Haettenschweiler', 'Harlow Solid Italic', 'Harrington', 'Heather', 'Heiti SC', 'Heiti TC', 'HELV',
0562         'Herald', 'High Tower Text', 'Hiragino Kaku Gothic ProN', 'Hiragino Mincho ProN', 'Hoefler Text', 'Humanst 521 Cn BT', 'Humanst521 BT', 'Humanst521 Lt BT', 'Imprint MT Shadow', 'Incised901 Bd BT', 'Incised901 BT',
0563         'Incised901 Lt BT', 'INCONSOLATA', 'Informal Roman', 'Informal011 BT', 'INTERSTATE', 'IrisUPC', 'Iskoola Pota', 'JasmineUPC', 'Jazz LET', 'Jenson', 'Jester', 'Jokerman', 'Juice ITC', 'Kabel Bk BT', 'Kabel Ult BT', 'Kailasa', 'KaiTi', 'Kalinga', 'Kannada Sangam MN',
0564         'Kartika', 'Kaufmann Bd BT', 'Kaufmann BT', 'Khmer UI', 'KodchiangUPC', 'Kokila', 'Korinna BT', 'Kristen ITC', 'Krungthep', 'Kunstler Script', 'Lao UI', 'Latha', 'Leelawadee', 'Letter Gothic', 'Levenim MT', 'LilyUPC', 'Lithograph', 'Lithograph Light', 'Long Island',
0565         'Lydian BT', 'Magneto', 'Maiandra GD', 'Malayalam Sangam MN', 'Malgun Gothic',
0566         'Mangal', 'Marigold', 'Marion', 'Marker Felt', 'Market', 'Marlett', 'Matisse ITC', 'Matura MT Script Capitals', 'Meiryo', 'Meiryo UI', 'Microsoft Himalaya', 'Microsoft JhengHei', 'Microsoft New Tai Lue', 'Microsoft PhagsPa', 'Microsoft Tai Le',
0567         'Microsoft Uighur', 'Microsoft YaHei', 'Microsoft Yi Baiti', 'MingLiU', 'MingLiU_HKSCS', 'MingLiU_HKSCS-ExtB', 'MingLiU-ExtB', 'Minion', 'Minion Pro', 'Miriam', 'Miriam Fixed', 'Mistral', 'Modern', 'Modern No. 20', 'Mona Lisa Solid ITC TT', 'Mongolian Baiti',
0568         'MONO', 'MoolBoran', 'Mrs Eaves', 'MS LineDraw', 'MS Mincho', 'MS PMincho', 'MS Reference Specialty', 'MS UI Gothic', 'MT Extra', 'MUSEO', 'MV Boli',
0569         'Nadeem', 'Narkisim', 'NEVIS', 'News Gothic', 'News GothicMT', 'NewsGoth BT', 'Niagara Engraved', 'Niagara Solid', 'Noteworthy', 'NSimSun', 'Nyala', 'OCR A Extended', 'Old Century', 'Old English Text MT', 'Onyx', 'Onyx BT', 'OPTIMA', 'Oriya Sangam MN',
0570         'OSAKA', 'OzHandicraft BT', 'Palace Script MT', 'Papyrus', 'Parchment', 'Party LET', 'Pegasus', 'Perpetua', 'Perpetua Titling MT', 'PetitaBold', 'Pickwick', 'Plantagenet Cherokee', 'Playbill', 'PMingLiU', 'PMingLiU-ExtB',
0571         'Poor Richard', 'Poster', 'PosterBodoni BT', 'PRINCETOWN LET', 'Pristina', 'PTBarnum BT', 'Pythagoras', 'Raavi', 'Rage Italic', 'Ravie', 'Ribbon131 Bd BT', 'Rockwell', 'Rockwell Condensed', 'Rockwell Extra Bold', 'Rod', 'Roman', 'Sakkal Majalla',
0572         'Santa Fe LET', 'Savoye LET', 'Sceptre', 'Script', 'Script MT Bold', 'SCRIPTINA', 'Serifa', 'Serifa BT', 'Serifa Th BT', 'ShelleyVolante BT', 'Sherwood',
0573         'Shonar Bangla', 'Showcard Gothic', 'Shruti', 'Signboard', 'SILKSCREEN', 'SimHei', 'Simplified Arabic', 'Simplified Arabic Fixed', 'SimSun', 'SimSun-ExtB', 'Sinhala Sangam MN', 'Sketch Rockwell', 'Skia', 'Small Fonts', 'Snap ITC', 'Snell Roundhand', 'Socket',
0574         'Souvenir Lt BT', 'Staccato222 BT', 'Steamer', 'Stencil', 'Storybook', 'Styllo', 'Subway', 'Swis721 BlkEx BT', 'Swiss911 XCm BT', 'Sylfaen', 'Synchro LET', 'System', 'Tamil Sangam MN', 'Technical', 'Teletype', 'Telugu Sangam MN', 'Tempus Sans ITC',
0575         'Terminal', 'Thonburi', 'Traditional Arabic', 'Trajan', 'TRAJAN PRO', 'Tristan', 'Tubular', 'Tunga', 'Tw Cen MT', 'Tw Cen MT Condensed', 'Tw Cen MT Condensed Extra Bold',
0576         'TypoUpright BT', 'Unicorn', 'Univers', 'Univers CE 55 Medium', 'Univers Condensed', 'Utsaah', 'Vagabond', 'Vani', 'Vijaya', 'Viner Hand ITC', 'VisualUI', 'Vivaldi', 'Vladimir Script', 'Vrinda', 'Westminster', 'WHITNEY', 'Wide Latin',
0577         'ZapfEllipt BT', 'ZapfHumnst BT', 'ZapfHumnst Dm BT', 'Zapfino', 'Zurich BlkEx BT', 'Zurich Ex BT', 'ZWAdobeF']
0578       fontList = fontList.concat(extendedFontList)
0579     }
0580 
0581     fontList = fontList.concat(options.fonts.userDefinedFonts)
0582 
0583     // remove duplicate fonts
0584     fontList = fontList.filter(function (font, position) {
0585       return fontList.indexOf(font) === position
0586     })
0587 
0588     // we use m or w because these two characters take up the maximum width.
0589     // And we use a LLi so that the same matching fonts can get separated
0590     var testString = 'mmmmmmmmmmlli'
0591 
0592     // we test using 72px font size, we may use any size. I guess larger the better.
0593     var testSize = '72px'
0594 
0595     var h = document.getElementsByTagName('body')[0]
0596 
0597     // div to load spans for the base fonts
0598     var baseFontsDiv = document.createElement('div')
0599 
0600     // div to load spans for the fonts to detect
0601     var fontsDiv = document.createElement('div')
0602 
0603     var defaultWidth = {}
0604     var defaultHeight = {}
0605 
0606     // creates a span where the fonts will be loaded
0607     var createSpan = function () {
0608       var s = document.createElement('span')
0609       /*
0610        * We need this css as in some weird browser this
0611        * span elements shows up for a microSec which creates a
0612        * bad user experience
0613        */
0614       s.style.position = 'absolute'
0615       s.style.left = '-9999px'
0616       s.style.fontSize = testSize
0617 
0618       // css font reset to reset external styles
0619       s.style.fontStyle = 'normal'
0620       s.style.fontWeight = 'normal'
0621       s.style.letterSpacing = 'normal'
0622       s.style.lineBreak = 'auto'
0623       s.style.lineHeight = 'normal'
0624       s.style.textTransform = 'none'
0625       s.style.textAlign = 'left'
0626       s.style.textDecoration = 'none'
0627       s.style.textShadow = 'none'
0628       s.style.whiteSpace = 'normal'
0629       s.style.wordBreak = 'normal'
0630       s.style.wordSpacing = 'normal'
0631 
0632       s.innerHTML = testString
0633       return s
0634     }
0635 
0636     // creates a span and load the font to detect and a base font for fallback
0637     var createSpanWithFonts = function (fontToDetect, baseFont) {
0638       var s = createSpan()
0639       s.style.fontFamily = "'" + fontToDetect + "'," + baseFont
0640       return s
0641     }
0642 
0643     // creates spans for the base fonts and adds them to baseFontsDiv
0644     var initializeBaseFontsSpans = function () {
0645       var spans = []
0646       for (var index = 0, length = baseFonts.length; index < length; index++) {
0647         var s = createSpan()
0648         s.style.fontFamily = baseFonts[index]
0649         baseFontsDiv.appendChild(s)
0650         spans.push(s)
0651       }
0652       return spans
0653     }
0654 
0655     // creates spans for the fonts to detect and adds them to fontsDiv
0656     var initializeFontsSpans = function () {
0657       var spans = {}
0658       for (var i = 0, l = fontList.length; i < l; i++) {
0659         var fontSpans = []
0660         for (var j = 0, numDefaultFonts = baseFonts.length; j < numDefaultFonts; j++) {
0661           var s = createSpanWithFonts(fontList[i], baseFonts[j])
0662           fontsDiv.appendChild(s)
0663           fontSpans.push(s)
0664         }
0665         spans[fontList[i]] = fontSpans // Stores {fontName : [spans for that font]}
0666       }
0667       return spans
0668     }
0669 
0670     // checks if a font is available
0671     var isFontAvailable = function (fontSpans) {
0672       var detected = false
0673       for (var i = 0; i < baseFonts.length; i++) {
0674         detected = (fontSpans[i].offsetWidth !== defaultWidth[baseFonts[i]] || fontSpans[i].offsetHeight !== defaultHeight[baseFonts[i]])
0675         if (detected) {
0676           return detected
0677         }
0678       }
0679       return detected
0680     }
0681 
0682     // create spans for base fonts
0683     var baseFontsSpans = initializeBaseFontsSpans()
0684 
0685     // add the spans to the DOM
0686     h.appendChild(baseFontsDiv)
0687 
0688     // get the default width for the three base fonts
0689     for (var index = 0, length = baseFonts.length; index < length; index++) {
0690       defaultWidth[baseFonts[index]] = baseFontsSpans[index].offsetWidth // width for the default font
0691       defaultHeight[baseFonts[index]] = baseFontsSpans[index].offsetHeight // height for the default font
0692     }
0693 
0694     // create spans for fonts to detect
0695     var fontsSpans = initializeFontsSpans()
0696 
0697     // add all the spans to the DOM
0698     h.appendChild(fontsDiv)
0699 
0700     // check available fonts
0701     var available = []
0702     for (var i = 0, l = fontList.length; i < l; i++) {
0703       if (isFontAvailable(fontsSpans[fontList[i]])) {
0704         available.push(fontList[i])
0705       }
0706     }
0707 
0708     // remove spans from DOM
0709     h.removeChild(fontsDiv)
0710     h.removeChild(baseFontsDiv)
0711     done(available)
0712   }
0713   var pluginsComponent = function (done, options) {
0714     if (isIE()) {
0715       if (!options.plugins.excludeIE) {
0716         done(getIEPlugins(options))
0717       } else {
0718         done(options.EXCLUDED)
0719       }
0720     } else {
0721       done(getRegularPlugins(options))
0722     }
0723   }
0724   var getRegularPlugins = function (options) {
0725     if (navigator.plugins == null) {
0726       return options.NOT_AVAILABLE
0727     }
0728 
0729     var plugins = []
0730     // plugins isn't defined in Node envs.
0731     for (var i = 0, l = navigator.plugins.length; i < l; i++) {
0732       if (navigator.plugins[i]) { plugins.push(navigator.plugins[i]) }
0733     }
0734 
0735     // sorting plugins only for those user agents, that we know randomize the plugins
0736     // every time we try to enumerate them
0737     if (pluginsShouldBeSorted(options)) {
0738       plugins = plugins.sort(function (a, b) {
0739         if (a.name > b.name) { return 1 }
0740         if (a.name < b.name) { return -1 }
0741         return 0
0742       })
0743     }
0744     return map(plugins, function (p) {
0745       var mimeTypes = map(p, function (mt) {
0746         return [mt.type, mt.suffixes]
0747       })
0748       return [p.name, p.description, mimeTypes]
0749     })
0750   }
0751   var getIEPlugins = function (options) {
0752     var result = []
0753     if ((Object.getOwnPropertyDescriptor && Object.getOwnPropertyDescriptor(window, 'ActiveXObject')) || ('ActiveXObject' in window)) {
0754       var names = [
0755         'AcroPDF.PDF', // Adobe PDF reader 7+
0756         'Adodb.Stream',
0757         'AgControl.AgControl', // Silverlight
0758         'DevalVRXCtrl.DevalVRXCtrl.1',
0759         'MacromediaFlashPaper.MacromediaFlashPaper',
0760         'Msxml2.DOMDocument',
0761         'Msxml2.XMLHTTP',
0762         'PDF.PdfCtrl', // Adobe PDF reader 6 and earlier, brrr
0763         'QuickTime.QuickTime', // QuickTime
0764         'QuickTimeCheckObject.QuickTimeCheck.1',
0765         'RealPlayer',
0766         'RealPlayer.RealPlayer(tm) ActiveX Control (32-bit)',
0767         'RealVideo.RealVideo(tm) ActiveX Control (32-bit)',
0768         'Scripting.Dictionary',
0769         'SWCtl.SWCtl', // ShockWave player
0770         'Shell.UIHelper',
0771         'ShockwaveFlash.ShockwaveFlash', // flash plugin
0772         'Skype.Detection',
0773         'TDCCtl.TDCCtl',
0774         'WMPlayer.OCX', // Windows media player
0775         'rmocx.RealPlayer G2 Control',
0776         'rmocx.RealPlayer G2 Control.1'
0777       ]
0778       // starting to detect plugins in IE
0779       result = map(names, function (name) {
0780         try {
0781           // eslint-disable-next-line no-new
0782           new window.ActiveXObject(name)
0783           return name
0784         } catch (e) {
0785           return options.ERROR
0786         }
0787       })
0788     } else {
0789       result.push(options.NOT_AVAILABLE)
0790     }
0791     if (navigator.plugins) {
0792       result = result.concat(getRegularPlugins(options))
0793     }
0794     return result
0795   }
0796   var pluginsShouldBeSorted = function (options) {
0797     var should = false
0798     for (var i = 0, l = options.plugins.sortPluginsFor.length; i < l; i++) {
0799       var re = options.plugins.sortPluginsFor[i]
0800       if (navigator.userAgent.match(re)) {
0801         should = true
0802         break
0803       }
0804     }
0805     return should
0806   }
0807   var touchSupportKey = function (done) {
0808     done(getTouchSupport())
0809   }
0810   var hardwareConcurrencyKey = function (done, options) {
0811     done(getHardwareConcurrency(options))
0812   }
0813   var hasSessionStorage = function (options) {
0814     try {
0815       return !!window.sessionStorage
0816     } catch (e) {
0817       return options.ERROR // SecurityError when referencing it means it exists
0818     }
0819   }
0820 
0821   // https://bugzilla.mozilla.org/show_bug.cgi?id=781447
0822   var hasLocalStorage = function (options) {
0823     try {
0824       return !!window.localStorage
0825     } catch (e) {
0826       return options.ERROR // SecurityError when referencing it means it exists
0827     }
0828   }
0829   var hasIndexedDB = function (options) {
0830     try {
0831       return !!window.indexedDB
0832     } catch (e) {
0833       return options.ERROR // SecurityError when referencing it means it exists
0834     }
0835   }
0836   var getHardwareConcurrency = function (options) {
0837     if (navigator.hardwareConcurrency) {
0838       return navigator.hardwareConcurrency
0839     }
0840     return options.NOT_AVAILABLE
0841   }
0842   var getNavigatorCpuClass = function (options) {
0843     return navigator.cpuClass || options.NOT_AVAILABLE
0844   }
0845   var getNavigatorPlatform = function (options) {
0846     if (navigator.platform) {
0847       return navigator.platform
0848     } else {
0849       return options.NOT_AVAILABLE
0850     }
0851   }
0852   var getDoNotTrack = function (options) {
0853     if (navigator.doNotTrack) {
0854       return navigator.doNotTrack
0855     } else if (navigator.msDoNotTrack) {
0856       return navigator.msDoNotTrack
0857     } else if (window.doNotTrack) {
0858       return window.doNotTrack
0859     } else {
0860       return options.NOT_AVAILABLE
0861     }
0862   }
0863   // This is a crude and primitive touch screen detection.
0864   // It's not possible to currently reliably detect the  availability of a touch screen
0865   // with a JS, without actually subscribing to a touch event.
0866   // http://www.stucox.com/blog/you-cant-detect-a-touchscreen/
0867   // https://github.com/Modernizr/Modernizr/issues/548
0868   // method returns an array of 3 values:
0869   // maxTouchPoints, the success or failure of creating a TouchEvent,
0870   // and the availability of the 'ontouchstart' property
0871 
0872   var getTouchSupport = function () {
0873     var maxTouchPoints = 0
0874     var touchEvent
0875     if (typeof navigator.maxTouchPoints !== 'undefined') {
0876       maxTouchPoints = navigator.maxTouchPoints
0877     } else if (typeof navigator.msMaxTouchPoints !== 'undefined') {
0878       maxTouchPoints = navigator.msMaxTouchPoints
0879     }
0880     try {
0881       document.createEvent('TouchEvent')
0882       touchEvent = true
0883     } catch (_) {
0884       touchEvent = false
0885     }
0886     var touchStart = 'ontouchstart' in window
0887     return [maxTouchPoints, touchEvent, touchStart]
0888   }
0889   // https://www.browserleaks.com/canvas#how-does-it-work
0890 
0891   var getCanvasFp = function (options) {
0892     var result = []
0893     // Very simple now, need to make it more complex (geo shapes etc)
0894     var canvas = document.createElement('canvas')
0895     canvas.width = 2000
0896     canvas.height = 200
0897     canvas.style.display = 'inline'
0898     var ctx = canvas.getContext('2d')
0899     // detect browser support of canvas winding
0900     // http://blogs.adobe.com/webplatform/2013/01/30/winding-rules-in-canvas/
0901     // https://github.com/Modernizr/Modernizr/blob/master/feature-detects/canvas/winding.js
0902     ctx.rect(0, 0, 10, 10)
0903     ctx.rect(2, 2, 6, 6)
0904     result.push('canvas winding:' + ((ctx.isPointInPath(5, 5, 'evenodd') === false) ? 'yes' : 'no'))
0905 
0906     ctx.textBaseline = 'alphabetic'
0907     ctx.fillStyle = '#f60'
0908     ctx.fillRect(125, 1, 62, 20)
0909     ctx.fillStyle = '#069'
0910     // https://github.com/Valve/fingerprintjs2/issues/66
0911     if (options.dontUseFakeFontInCanvas) {
0912       ctx.font = '11pt Arial'
0913     } else {
0914       ctx.font = '11pt no-real-font-123'
0915     }
0916     ctx.fillText('Cwm fjordbank glyphs vext quiz, \ud83d\ude03', 2, 15)
0917     ctx.fillStyle = 'rgba(102, 204, 0, 0.2)'
0918     ctx.font = '18pt Arial'
0919     ctx.fillText('Cwm fjordbank glyphs vext quiz, \ud83d\ude03', 4, 45)
0920 
0921     // canvas blending
0922     // http://blogs.adobe.com/webplatform/2013/01/28/blending-features-in-canvas/
0923     // http://jsfiddle.net/NDYV8/16/
0924     ctx.globalCompositeOperation = 'multiply'
0925     ctx.fillStyle = 'rgb(255,0,255)'
0926     ctx.beginPath()
0927     ctx.arc(50, 50, 50, 0, Math.PI * 2, true)
0928     ctx.closePath()
0929     ctx.fill()
0930     ctx.fillStyle = 'rgb(0,255,255)'
0931     ctx.beginPath()
0932     ctx.arc(100, 50, 50, 0, Math.PI * 2, true)
0933     ctx.closePath()
0934     ctx.fill()
0935     ctx.fillStyle = 'rgb(255,255,0)'
0936     ctx.beginPath()
0937     ctx.arc(75, 100, 50, 0, Math.PI * 2, true)
0938     ctx.closePath()
0939     ctx.fill()
0940     ctx.fillStyle = 'rgb(255,0,255)'
0941     // canvas winding
0942     // http://blogs.adobe.com/webplatform/2013/01/30/winding-rules-in-canvas/
0943     // http://jsfiddle.net/NDYV8/19/
0944     ctx.arc(75, 75, 75, 0, Math.PI * 2, true)
0945     ctx.arc(75, 75, 25, 0, Math.PI * 2, true)
0946     ctx.fill('evenodd')
0947 
0948     if (canvas.toDataURL) { result.push('canvas fp:' + canvas.toDataURL()) }
0949     return result
0950   }
0951   var getWebglFp = function () {
0952     var gl
0953     var fa2s = function (fa) {
0954       gl.clearColor(0.0, 0.0, 0.0, 1.0)
0955       gl.enable(gl.DEPTH_TEST)
0956       gl.depthFunc(gl.LEQUAL)
0957       gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
0958       return '[' + fa[0] + ', ' + fa[1] + ']'
0959     }
0960     var maxAnisotropy = function (gl) {
0961       var ext = gl.getExtension('EXT_texture_filter_anisotropic') || gl.getExtension('WEBKIT_EXT_texture_filter_anisotropic') || gl.getExtension('MOZ_EXT_texture_filter_anisotropic')
0962       if (ext) {
0963         var anisotropy = gl.getParameter(ext.MAX_TEXTURE_MAX_ANISOTROPY_EXT)
0964         if (anisotropy === 0) {
0965           anisotropy = 2
0966         }
0967         return anisotropy
0968       } else {
0969         return null
0970       }
0971     }
0972 
0973     gl = getWebglCanvas()
0974     if (!gl) { return null }
0975     // WebGL fingerprinting is a combination of techniques, found in MaxMind antifraud script & Augur fingerprinting.
0976     // First it draws a gradient object with shaders and convers the image to the Base64 string.
0977     // Then it enumerates all WebGL extensions & capabilities and appends them to the Base64 string, resulting in a huge WebGL string, potentially very unique on each device
0978     // Since iOS supports webgl starting from version 8.1 and 8.1 runs on several graphics chips, the results may be different across ios devices, but we need to verify it.
0979     var result = []
0980     var vShaderTemplate = 'attribute vec2 attrVertex;varying vec2 varyinTexCoordinate;uniform vec2 uniformOffset;void main(){varyinTexCoordinate=attrVertex+uniformOffset;gl_Position=vec4(attrVertex,0,1);}'
0981     var fShaderTemplate = 'precision mediump float;varying vec2 varyinTexCoordinate;void main() {gl_FragColor=vec4(varyinTexCoordinate,0,1);}'
0982     var vertexPosBuffer = gl.createBuffer()
0983     gl.bindBuffer(gl.ARRAY_BUFFER, vertexPosBuffer)
0984     var vertices = new Float32Array([-0.2, -0.9, 0, 0.4, -0.26, 0, 0, 0.732134444, 0])
0985     gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW)
0986     vertexPosBuffer.itemSize = 3
0987     vertexPosBuffer.numItems = 3
0988     var program = gl.createProgram()
0989     var vshader = gl.createShader(gl.VERTEX_SHADER)
0990     gl.shaderSource(vshader, vShaderTemplate)
0991     gl.compileShader(vshader)
0992     var fshader = gl.createShader(gl.FRAGMENT_SHADER)
0993     gl.shaderSource(fshader, fShaderTemplate)
0994     gl.compileShader(fshader)
0995     gl.attachShader(program, vshader)
0996     gl.attachShader(program, fshader)
0997     gl.linkProgram(program)
0998     gl.useProgram(program)
0999     program.vertexPosAttrib = gl.getAttribLocation(program, 'attrVertex')
1000     program.offsetUniform = gl.getUniformLocation(program, 'uniformOffset')
1001     gl.enableVertexAttribArray(program.vertexPosArray)
1002     gl.vertexAttribPointer(program.vertexPosAttrib, vertexPosBuffer.itemSize, gl.FLOAT, !1, 0, 0)
1003     gl.uniform2f(program.offsetUniform, 1, 1)
1004     gl.drawArrays(gl.TRIANGLE_STRIP, 0, vertexPosBuffer.numItems)
1005     try {
1006       result.push(gl.canvas.toDataURL())
1007     } catch (e) {
1008       /* .toDataURL may be absent or broken (blocked by extension) */
1009     }
1010     result.push('extensions:' + (gl.getSupportedExtensions() || []).join(';'))
1011     result.push('webgl aliased line width range:' + fa2s(gl.getParameter(gl.ALIASED_LINE_WIDTH_RANGE)))
1012     result.push('webgl aliased point size range:' + fa2s(gl.getParameter(gl.ALIASED_POINT_SIZE_RANGE)))
1013     result.push('webgl alpha bits:' + gl.getParameter(gl.ALPHA_BITS))
1014     result.push('webgl antialiasing:' + (gl.getContextAttributes().antialias ? 'yes' : 'no'))
1015     result.push('webgl blue bits:' + gl.getParameter(gl.BLUE_BITS))
1016     result.push('webgl depth bits:' + gl.getParameter(gl.DEPTH_BITS))
1017     result.push('webgl green bits:' + gl.getParameter(gl.GREEN_BITS))
1018     result.push('webgl max anisotropy:' + maxAnisotropy(gl))
1019     result.push('webgl max combined texture image units:' + gl.getParameter(gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS))
1020     result.push('webgl max cube map texture size:' + gl.getParameter(gl.MAX_CUBE_MAP_TEXTURE_SIZE))
1021     result.push('webgl max fragment uniform vectors:' + gl.getParameter(gl.MAX_FRAGMENT_UNIFORM_VECTORS))
1022     result.push('webgl max render buffer size:' + gl.getParameter(gl.MAX_RENDERBUFFER_SIZE))
1023     result.push('webgl max texture image units:' + gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS))
1024     result.push('webgl max texture size:' + gl.getParameter(gl.MAX_TEXTURE_SIZE))
1025     result.push('webgl max varying vectors:' + gl.getParameter(gl.MAX_VARYING_VECTORS))
1026     result.push('webgl max vertex attribs:' + gl.getParameter(gl.MAX_VERTEX_ATTRIBS))
1027     result.push('webgl max vertex texture image units:' + gl.getParameter(gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS))
1028     result.push('webgl max vertex uniform vectors:' + gl.getParameter(gl.MAX_VERTEX_UNIFORM_VECTORS))
1029     result.push('webgl max viewport dims:' + fa2s(gl.getParameter(gl.MAX_VIEWPORT_DIMS)))
1030     result.push('webgl red bits:' + gl.getParameter(gl.RED_BITS))
1031     result.push('webgl renderer:' + gl.getParameter(gl.RENDERER))
1032     result.push('webgl shading language version:' + gl.getParameter(gl.SHADING_LANGUAGE_VERSION))
1033     result.push('webgl stencil bits:' + gl.getParameter(gl.STENCIL_BITS))
1034     result.push('webgl vendor:' + gl.getParameter(gl.VENDOR))
1035     result.push('webgl version:' + gl.getParameter(gl.VERSION))
1036 
1037     try {
1038       // Add the unmasked vendor and unmasked renderer if the debug_renderer_info extension is available
1039       var extensionDebugRendererInfo = gl.getExtension('WEBGL_debug_renderer_info')
1040       if (extensionDebugRendererInfo) {
1041         result.push('webgl unmasked vendor:' + gl.getParameter(extensionDebugRendererInfo.UNMASKED_VENDOR_WEBGL))
1042         result.push('webgl unmasked renderer:' + gl.getParameter(extensionDebugRendererInfo.UNMASKED_RENDERER_WEBGL))
1043       }
1044     } catch (e) { /* squelch */ }
1045 
1046     if (!gl.getShaderPrecisionFormat) {
1047       return result
1048     }
1049 
1050     each(['FLOAT', 'INT'], function (numType) {
1051       each(['VERTEX', 'FRAGMENT'], function (shader) {
1052         each(['HIGH', 'MEDIUM', 'LOW'], function (numSize) {
1053           each(['precision', 'rangeMin', 'rangeMax'], function (key) {
1054             var format = gl.getShaderPrecisionFormat(gl[shader + '_SHADER'], gl[numSize + '_' + numType])[key]
1055             if (key !== 'precision') {
1056               key = 'precision ' + key
1057             }
1058             var line = ['webgl ', shader.toLowerCase(), ' shader ', numSize.toLowerCase(), ' ', numType.toLowerCase(), ' ', key, ':', format].join('')
1059             result.push(line)
1060           })
1061         })
1062       })
1063     })
1064     return result
1065   }
1066   var getWebglVendorAndRenderer = function () {
1067     /* This a subset of the WebGL fingerprint with a lot of entropy, while being reasonably browser-independent */
1068     try {
1069       var glContext = getWebglCanvas()
1070       var extensionDebugRendererInfo = glContext.getExtension('WEBGL_debug_renderer_info')
1071       return glContext.getParameter(extensionDebugRendererInfo.UNMASKED_VENDOR_WEBGL) + '~' + glContext.getParameter(extensionDebugRendererInfo.UNMASKED_RENDERER_WEBGL)
1072     } catch (e) {
1073       return null
1074     }
1075   }
1076   var getAdBlock = function () {
1077     var ads = document.createElement('div')
1078     ads.innerHTML = '&nbsp;'
1079     ads.className = 'adsbox'
1080     var result = false
1081     try {
1082       // body may not exist, that's why we need try/catch
1083       document.body.appendChild(ads)
1084       result = document.getElementsByClassName('adsbox')[0].offsetHeight === 0
1085       document.body.removeChild(ads)
1086     } catch (e) {
1087       result = false
1088     }
1089     return result
1090   }
1091   var getHasLiedLanguages = function () {
1092     // We check if navigator.language is equal to the first language of navigator.languages
1093     // navigator.languages is undefined on IE11 (and potentially older IEs)
1094     if (typeof navigator.languages !== 'undefined') {
1095       try {
1096         var firstLanguages = navigator.languages[0].substr(0, 2)
1097         if (firstLanguages !== navigator.language.substr(0, 2)) {
1098           return true
1099         }
1100       } catch (err) {
1101         return true
1102       }
1103     }
1104     return false
1105   }
1106   var getHasLiedResolution = function () {
1107     return window.screen.width < window.screen.availWidth || window.screen.height < window.screen.availHeight
1108   }
1109   var getHasLiedOs = function () {
1110     var userAgent = navigator.userAgent.toLowerCase()
1111     var oscpu = navigator.oscpu
1112     var platform = navigator.platform.toLowerCase()
1113     var os
1114     // We extract the OS from the user agent (respect the order of the if else if statement)
1115     if (userAgent.indexOf('windows phone') >= 0) {
1116       os = 'Windows Phone'
1117     } else if (userAgent.indexOf('win') >= 0) {
1118       os = 'Windows'
1119     } else if (userAgent.indexOf('android') >= 0) {
1120       os = 'Android'
1121     } else if (userAgent.indexOf('linux') >= 0 || userAgent.indexOf('cros') >= 0) {
1122       os = 'Linux'
1123     } else if (userAgent.indexOf('iphone') >= 0 || userAgent.indexOf('ipad') >= 0) {
1124       os = 'iOS'
1125     } else if (userAgent.indexOf('mac') >= 0) {
1126       os = 'Mac'
1127     } else {
1128       os = 'Other'
1129     }
1130     // We detect if the person uses a mobile device
1131     var mobileDevice = (('ontouchstart' in window) ||
1132       (navigator.maxTouchPoints > 0) ||
1133       (navigator.msMaxTouchPoints > 0))
1134 
1135     if (mobileDevice && os !== 'Windows Phone' && os !== 'Android' && os !== 'iOS' && os !== 'Other') {
1136       return true
1137     }
1138 
1139     // We compare oscpu with the OS extracted from the UA
1140     if (typeof oscpu !== 'undefined') {
1141       oscpu = oscpu.toLowerCase()
1142       if (oscpu.indexOf('win') >= 0 && os !== 'Windows' && os !== 'Windows Phone') {
1143         return true
1144       } else if (oscpu.indexOf('linux') >= 0 && os !== 'Linux' && os !== 'Android') {
1145         return true
1146       } else if (oscpu.indexOf('mac') >= 0 && os !== 'Mac' && os !== 'iOS') {
1147         return true
1148       } else if ((oscpu.indexOf('win') === -1 && oscpu.indexOf('linux') === -1 && oscpu.indexOf('mac') === -1) !== (os === 'Other')) {
1149         return true
1150       }
1151     }
1152 
1153     // We compare platform with the OS extracted from the UA
1154     if (platform.indexOf('win') >= 0 && os !== 'Windows' && os !== 'Windows Phone') {
1155       return true
1156     } else if ((platform.indexOf('linux') >= 0 || platform.indexOf('android') >= 0 || platform.indexOf('pike') >= 0) && os !== 'Linux' && os !== 'Android') {
1157       return true
1158     } else if ((platform.indexOf('mac') >= 0 || platform.indexOf('ipad') >= 0 || platform.indexOf('ipod') >= 0 || platform.indexOf('iphone') >= 0) && os !== 'Mac' && os !== 'iOS') {
1159       return true
1160     } else {
1161       var platformIsOther = platform.indexOf('win') < 0 &&
1162         platform.indexOf('linux') < 0 &&
1163         platform.indexOf('mac') < 0 &&
1164         platform.indexOf('iphone') < 0 &&
1165         platform.indexOf('ipad') < 0
1166       if (platformIsOther !== (os === 'Other')) {
1167         return true
1168       }
1169     }
1170 
1171     return typeof navigator.plugins === 'undefined' && os !== 'Windows' && os !== 'Windows Phone'
1172   }
1173   var getHasLiedBrowser = function () {
1174     var userAgent = navigator.userAgent.toLowerCase()
1175     var productSub = navigator.productSub
1176 
1177     // we extract the browser from the user agent (respect the order of the tests)
1178     var browser
1179     if (userAgent.indexOf('firefox') >= 0) {
1180       browser = 'Firefox'
1181     } else if (userAgent.indexOf('opera') >= 0 || userAgent.indexOf('opr') >= 0) {
1182       browser = 'Opera'
1183     } else if (userAgent.indexOf('chrome') >= 0) {
1184       browser = 'Chrome'
1185     } else if (userAgent.indexOf('safari') >= 0) {
1186       browser = 'Safari'
1187     } else if (userAgent.indexOf('trident') >= 0) {
1188       browser = 'Internet Explorer'
1189     } else {
1190       browser = 'Other'
1191     }
1192 
1193     if ((browser === 'Chrome' || browser === 'Safari' || browser === 'Opera') && productSub !== '20030107') {
1194       return true
1195     }
1196 
1197     // eslint-disable-next-line no-eval
1198     var tempRes = eval.toString().length
1199     if (tempRes === 37 && browser !== 'Safari' && browser !== 'Firefox' && browser !== 'Other') {
1200       return true
1201     } else if (tempRes === 39 && browser !== 'Internet Explorer' && browser !== 'Other') {
1202       return true
1203     } else if (tempRes === 33 && browser !== 'Chrome' && browser !== 'Opera' && browser !== 'Other') {
1204       return true
1205     }
1206 
1207     // We create an error to see how it is handled
1208     var errFirefox
1209     try {
1210       // eslint-disable-next-line no-throw-literal
1211       throw 'a'
1212     } catch (err) {
1213       try {
1214         err.toSource()
1215         errFirefox = true
1216       } catch (errOfErr) {
1217         errFirefox = false
1218       }
1219     }
1220     return errFirefox && browser !== 'Firefox' && browser !== 'Other'
1221   }
1222   var isCanvasSupported = function () {
1223     var elem = document.createElement('canvas')
1224     return !!(elem.getContext && elem.getContext('2d'))
1225   }
1226   var isWebGlSupported = function () {
1227     // code taken from Modernizr
1228     if (!isCanvasSupported()) {
1229       return false
1230     }
1231 
1232     var glContext = getWebglCanvas()
1233     return !!window.WebGLRenderingContext && !!glContext
1234   }
1235   var isIE = function () {
1236     if (navigator.appName === 'Microsoft Internet Explorer') {
1237       return true
1238     } else if (navigator.appName === 'Netscape' && /Trident/.test(navigator.userAgent)) { // IE 11
1239       return true
1240     }
1241     return false
1242   }
1243   var hasSwfObjectLoaded = function () {
1244     return typeof window.swfobject !== 'undefined'
1245   }
1246   var hasMinFlashInstalled = function () {
1247     return window.swfobject.hasFlashPlayerVersion('9.0.0')
1248   }
1249   var addFlashDivNode = function (options) {
1250     var node = document.createElement('div')
1251     node.setAttribute('id', options.fonts.swfContainerId)
1252     document.body.appendChild(node)
1253   }
1254   var loadSwfAndDetectFonts = function (done, options) {
1255     var hiddenCallback = '___fp_swf_loaded'
1256     window[hiddenCallback] = function (fonts) {
1257       done(fonts)
1258     }
1259     var id = options.fonts.swfContainerId
1260     addFlashDivNode()
1261     var flashvars = { onReady: hiddenCallback }
1262     var flashparams = { allowScriptAccess: 'always', menu: 'false' }
1263     window.swfobject.embedSWF(options.fonts.swfPath, id, '1', '1', '9.0.0', false, flashvars, flashparams, {})
1264   }
1265   var getWebglCanvas = function () {
1266     var canvas = document.createElement('canvas')
1267     var gl = null
1268     try {
1269       gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl')
1270     } catch (e) { /* squelch */ }
1271     if (!gl) { gl = null }
1272     return gl
1273   }
1274 
1275   var components = [
1276     { key: 'userAgent', getData: UserAgent },
1277     { key: 'webdriver', getData: webdriver },
1278     { key: 'language', getData: languageKey },
1279     { key: 'colorDepth', getData: colorDepthKey },
1280     { key: 'deviceMemory', getData: deviceMemoryKey },
1281     { key: 'pixelRatio', getData: pixelRatioKey },
1282     { key: 'hardwareConcurrency', getData: hardwareConcurrencyKey },
1283     { key: 'screenResolution', getData: screenResolutionKey },
1284     { key: 'availableScreenResolution', getData: availableScreenResolutionKey },
1285     { key: 'timezoneOffset', getData: timezoneOffset },
1286     { key: 'timezone', getData: timezone },
1287     { key: 'sessionStorage', getData: sessionStorageKey },
1288     { key: 'localStorage', getData: localStorageKey },
1289     { key: 'indexedDb', getData: indexedDbKey },
1290     { key: 'addBehavior', getData: addBehaviorKey },
1291     { key: 'openDatabase', getData: openDatabaseKey },
1292     { key: 'cpuClass', getData: cpuClassKey },
1293     { key: 'platform', getData: platformKey },
1294     { key: 'doNotTrack', getData: doNotTrackKey },
1295     { key: 'plugins', getData: pluginsComponent },
1296     { key: 'canvas', getData: canvasKey },
1297     { key: 'webgl', getData: webglKey },
1298     { key: 'webglVendorAndRenderer', getData: webglVendorAndRendererKey },
1299     { key: 'adBlock', getData: adBlockKey },
1300     { key: 'hasLiedLanguages', getData: hasLiedLanguagesKey },
1301     { key: 'hasLiedResolution', getData: hasLiedResolutionKey },
1302     { key: 'hasLiedOs', getData: hasLiedOsKey },
1303     { key: 'hasLiedBrowser', getData: hasLiedBrowserKey },
1304     { key: 'touchSupport', getData: touchSupportKey },
1305     { key: 'fonts', getData: jsFontsKey, pauseBefore: true },
1306     { key: 'fontsFlash', getData: flashFontsKey, pauseBefore: true },
1307     { key: 'audio', getData: audioKey },
1308     { key: 'enumerateDevices', getData: enumerateDevicesKey }
1309   ]
1310 
1311   var Fingerprint2 = function (options) {
1312     throw new Error("'new Fingerprint()' is deprecated, see https://github.com/Valve/fingerprintjs2#upgrade-guide-from-182-to-200")
1313   }
1314 
1315   Fingerprint2.get = function (options, callback) {
1316     if (!callback) {
1317       callback = options
1318       options = {}
1319     } else if (!options) {
1320       options = {}
1321     }
1322     extendSoft(options, defaultOptions)
1323     options.components = options.extraComponents.concat(components)
1324 
1325     var keys = {
1326       data: [],
1327       addPreprocessedComponent: function (key, value) {
1328         if (typeof options.preprocessor === 'function') {
1329           value = options.preprocessor(key, value)
1330         }
1331         keys.data.push({ key: key, value: value })
1332       }
1333     }
1334 
1335     var i = -1
1336     var chainComponents = function (alreadyWaited) {
1337       i += 1
1338       if (i >= options.components.length) { // on finish
1339         callback(keys.data)
1340         return
1341       }
1342       var component = options.components[i]
1343 
1344       if (options.excludes[component.key]) {
1345         chainComponents(false) // skip
1346         return
1347       }
1348 
1349       if (!alreadyWaited && component.pauseBefore) {
1350         i -= 1
1351         setTimeout(function () {
1352           chainComponents(true)
1353         }, 1)
1354         return
1355       }
1356 
1357       try {
1358         component.getData(function (value) {
1359           keys.addPreprocessedComponent(component.key, value)
1360           chainComponents(false)
1361         }, options)
1362       } catch (error) {
1363         // main body error
1364         keys.addPreprocessedComponent(component.key, String(error))
1365         chainComponents(false)
1366       }
1367     }
1368 
1369     chainComponents(false)
1370   }
1371 
1372   Fingerprint2.getPromise = function (options) {
1373     return new Promise(function (resolve, reject) {
1374       Fingerprint2.get(options, resolve)
1375     })
1376   }
1377 
1378   Fingerprint2.getV18 = function (options, callback) {
1379     if (callback == null) {
1380       callback = options
1381       options = {}
1382     }
1383     return Fingerprint2.get(options, function (components) {
1384       var newComponents = []
1385       for (var i = 0; i < components.length; i++) {
1386         var component = components[i]
1387         if (component.value === (options.NOT_AVAILABLE || 'not available')) {
1388           newComponents.push({ key: component.key, value: 'unknown' })
1389         } else if (component.key === 'plugins') {
1390           newComponents.push({
1391             key: 'plugins',
1392             value: map(component.value, function (p) {
1393               var mimeTypes = map(p[2], function (mt) {
1394                 if (mt.join) { return mt.join('~') }
1395                 return mt
1396               }).join(',')
1397               return [p[0], p[1], mimeTypes].join('::')
1398             })
1399           })
1400         } else if (['canvas', 'webgl'].indexOf(component.key) !== -1) {
1401           newComponents.push({ key: component.key, value: component.value.join('~') })
1402         } else if (['sessionStorage', 'localStorage', 'indexedDb', 'addBehavior', 'openDatabase'].indexOf(component.key) !== -1) {
1403           if (component.value) {
1404             newComponents.push({ key: component.key, value: 1 })
1405           } else {
1406             // skip
1407             continue
1408           }
1409         } else {
1410           if (component.value) {
1411             newComponents.push(component.value.join ? { key: component.key, value: component.value.join(';') } : component)
1412           } else {
1413             newComponents.push({ key: component.key, value: component.value })
1414           }
1415         }
1416       }
1417       var murmur = x64hash128(map(newComponents, function (component) { return component.value }).join('~~~'), 31)
1418       callback(murmur, newComponents)
1419     })
1420   }
1421 
1422   Fingerprint2.x64hash128 = x64hash128
1423   Fingerprint2.VERSION = '2.1.0'
1424   return Fingerprint2
1425 })