diff --git a/data/web/inc/footer.inc.php b/data/web/inc/footer.inc.php index 9246d230..3ba758be 100644 --- a/data/web/inc/footer.inc.php +++ b/data/web/inc/footer.inc.php @@ -40,6 +40,7 @@ $(document).ready(function() { backdrop: 'static', keyboard: false }); + $('#u2f_status_auth').html('

Initializing, please wait...

'); $('#ConfirmTFAModal').on('shown.bs.modal', function(){ $(this).find('#token').focus(); // If U2F @@ -49,20 +50,21 @@ $(document).ready(function() { cache: false, dataType: 'script', url: "/api/v1/get/u2f-authentication/", - success: function(data){ + complete: function(data){ + $('#u2f_status_auth').html(''); data; + setTimeout(function() { + console.log("Ready to authenticate"); + u2f.sign(appId, challenge, registeredKeys, function(data) { + var form = document.getElementById('u2f_auth_form'); + var auth = document.getElementById('u2f_auth_data'); + console.log("Authenticate callback", data); + auth.value = JSON.stringify(data); + form.submit(); + }); + }, 1000); } }); - setTimeout(function() { - console.log("sign: ", req); - u2f.sign(req, function(data) { - var form = document.getElementById('u2f_auth_form'); - var auth = document.getElementById('u2f_auth_data'); - console.log("Authenticate callback", data); - auth.value = JSON.stringify(data); - form.submit(); - }); - }, 1000); } }); @@ -81,32 +83,34 @@ $(document).ready(function() { if ($(this).val() == "u2f") { $('#U2FModal').modal('show'); $("option:selected").prop("selected", false); + $('#u2f_status_reg').html('

Initializing, please wait...

'); $.ajax({ type: "GET", cache: false, dataType: 'script', url: "/api/v1/get/u2f-registration/", - success: function(data){ + complete: function(data){ data; + setTimeout(function() { + console.log("Ready to register"); + $('#u2f_status_reg').html(''); + u2f.register(appId, registerRequests, registeredKeys, function(deviceResponse) { + var form = document.getElementById('u2f_reg_form'); + var reg = document.getElementById('u2f_register_data'); + console.log("Register callback: ", data); + if (deviceResponse.errorCode && deviceResponse.errorCode != 0) { + var u2f_return_code = document.getElementById('u2f_return_code'); + u2f_return_code.style.display = u2f_return_code.style.display === 'none' ? '' : null; + if (deviceResponse.errorCode == "4") { deviceResponse.errorCode = "4 - The presented device is not eligible for this request. For a registration request this may mean that the token is already registered, and for a sign request it may mean that the token does not know the presented key handle"; } + u2f_return_code.innerHTML = 'Error code: ' + deviceResponse.errorCode; + return; + } + reg.value = JSON.stringify(deviceResponse); + form.submit(); + }); + }, 1000); } }); - setTimeout(function() { - console.log("Register: ", req); - u2f.register([req], sigs, function(data) { - var form = document.getElementById('u2f_reg_form'); - var reg = document.getElementById('u2f_register_data'); - console.log("Register callback", data); - if (data.errorCode && data.errorCode != 0) { - var u2f_return_code = document.getElementById('u2f_return_code'); - u2f_return_code.style.display = u2f_return_code.style.display === 'none' ? '' : null; - if (data.errorCode == "4") { data.errorCode = "4 - The presented device is not eligible for this request. For a registration request this may mean that the token is already registered, and for a sign request it may mean that the token does not know the presented key handle"; } - u2f_return_code.innerHTML = 'Error code: ' + data.errorCode; - return; - } - reg.value = JSON.stringify(data); - form.submit(); - }); - }, 1000); } if ($(this).val() == "none") { $('#DisableTFAModal').modal('show'); @@ -212,4 +216,4 @@ $(document).ready(function() { =5.3.0" - }, - "require-dev": { - "phpunit/phpunit": "@stable" - }, - "time": "2017-02-17T15:24:54+00:00", - "type": "library", - "installation-source": "dist", - "autoload": { - "psr-4": { - "RobThree\\Auth\\": "lib" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Rob Janssen", - "homepage": "http://robiii.me", - "role": "Developer" - } - ], - "description": "Two Factor Authentication", - "homepage": "https://github.com/RobThree/TwoFactorAuth", - "keywords": [ - "Authentication", - "MFA", - "Multi Factor Authentication", - "Two Factor Authentication", - "authenticator", - "authy", - "php", - "tfa" - ] - }, { "name": "yubico/u2flib-server", "version": "1.0.1", @@ -90,18 +37,71 @@ "homepage": "https://developers.yubico.com/php-u2flib-server" }, { - "name": "phpmailer/phpmailer", - "version": "v5.2.25", - "version_normalized": "5.2.25.0", + "name": "robthree/twofactorauth", + "version": "1.6.1", + "version_normalized": "1.6.1.0", "source": { "type": "git", - "url": "https://github.com/PHPMailer/PHPMailer.git", - "reference": "2baf20b01690fba8cf720c1ebcf9b988eda50915" + "url": "https://github.com/RobThree/TwoFactorAuth.git", + "reference": "a77e7d822343bb88112baef808839cfae7bc5abb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/2baf20b01690fba8cf720c1ebcf9b988eda50915", - "reference": "2baf20b01690fba8cf720c1ebcf9b988eda50915", + "url": "https://api.github.com/repos/RobThree/TwoFactorAuth/zipball/a77e7d822343bb88112baef808839cfae7bc5abb", + "reference": "a77e7d822343bb88112baef808839cfae7bc5abb", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "@stable" + }, + "time": "2017-11-06T17:55:56+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "RobThree\\Auth\\": "lib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Rob Janssen", + "homepage": "http://robiii.me", + "role": "Developer" + } + ], + "description": "Two Factor Authentication", + "homepage": "https://github.com/RobThree/TwoFactorAuth", + "keywords": [ + "Authentication", + "MFA", + "Multi Factor Authentication", + "Two Factor Authentication", + "authenticator", + "authy", + "php", + "tfa" + ] + }, + { + "name": "phpmailer/phpmailer", + "version": "v5.2.26", + "version_normalized": "5.2.26.0", + "source": { + "type": "git", + "url": "https://github.com/PHPMailer/PHPMailer.git", + "reference": "70362997bda4376378be7d92d81e2200550923f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/70362997bda4376378be7d92d81e2200550923f7", + "reference": "70362997bda4376378be7d92d81e2200550923f7", "shasum": "" }, "require": { @@ -131,7 +131,7 @@ "suggest": { "league/oauth2-google": "Needed for Google XOAUTH2 authentication" }, - "time": "2017-08-28T11:12:07+00:00", + "time": "2017-11-04T09:26:05+00:00", "type": "library", "installation-source": "dist", "autoload": { diff --git a/data/web/inc/lib/vendor/phpmailer/phpmailer/VERSION b/data/web/inc/lib/vendor/phpmailer/phpmailer/VERSION index f23b9706..f0fb1a22 100644 --- a/data/web/inc/lib/vendor/phpmailer/phpmailer/VERSION +++ b/data/web/inc/lib/vendor/phpmailer/phpmailer/VERSION @@ -1 +1 @@ -5.2.25 +5.2.26 diff --git a/data/web/inc/lib/vendor/phpmailer/phpmailer/class.phpmailer.php b/data/web/inc/lib/vendor/phpmailer/phpmailer/class.phpmailer.php index 8042b384..99f9092c 100644 --- a/data/web/inc/lib/vendor/phpmailer/phpmailer/class.phpmailer.php +++ b/data/web/inc/lib/vendor/phpmailer/phpmailer/class.phpmailer.php @@ -31,7 +31,7 @@ class PHPMailer * The PHPMailer Version number. * @var string */ - public $Version = '5.2.25'; + public $Version = '5.2.26'; /** * Email priority. @@ -659,6 +659,8 @@ class PHPMailer if ($exceptions !== null) { $this->exceptions = (boolean)$exceptions; } + //Pick an appropriate debug output format automatically + $this->Debugoutput = (strpos(PHP_SAPI, 'cli') !== false ? 'echo' : 'html'); } /** diff --git a/data/web/inc/lib/vendor/phpmailer/phpmailer/class.pop3.php b/data/web/inc/lib/vendor/phpmailer/phpmailer/class.pop3.php index f2c4e374..f833ac61 100644 --- a/data/web/inc/lib/vendor/phpmailer/phpmailer/class.pop3.php +++ b/data/web/inc/lib/vendor/phpmailer/phpmailer/class.pop3.php @@ -34,7 +34,7 @@ class POP3 * @var string * @access public */ - public $Version = '5.2.25'; + public $Version = '5.2.26'; /** * Default POP3 port number. diff --git a/data/web/inc/lib/vendor/phpmailer/phpmailer/class.smtp.php b/data/web/inc/lib/vendor/phpmailer/phpmailer/class.smtp.php index d8af427e..be6ddce4 100644 --- a/data/web/inc/lib/vendor/phpmailer/phpmailer/class.smtp.php +++ b/data/web/inc/lib/vendor/phpmailer/phpmailer/class.smtp.php @@ -30,7 +30,7 @@ class SMTP * The PHPMailer SMTP version number. * @var string */ - const VERSION = '5.2.25'; + const VERSION = '5.2.26'; /** * SMTP line break constant. @@ -81,7 +81,7 @@ class SMTP * @deprecated Use the `VERSION` constant instead * @see SMTP::VERSION */ - public $Version = '5.2.25'; + public $Version = '5.2.26'; /** * SMTP server port number. diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/.gitignore b/data/web/inc/lib/vendor/robthree/twofactorauth/.gitignore index 1a31666a..8a25841c 100644 --- a/data/web/inc/lib/vendor/robthree/twofactorauth/.gitignore +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/.gitignore @@ -183,4 +183,7 @@ UpgradeLog*.htm FakesAssemblies/ # Composer -/vendor \ No newline at end of file +/vendor + +# .vs +.vs/ \ No newline at end of file diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/.travis.yml b/data/web/inc/lib/vendor/robthree/twofactorauth/.travis.yml index 034653bb..204dc63a 100644 --- a/data/web/inc/lib/vendor/robthree/twofactorauth/.travis.yml +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/.travis.yml @@ -1,11 +1,18 @@ language: php +dist: trusty +matrix: + include: + - php: 5.3 + dist: precise + php: - - 5.3 - 5.4 - 5.5 - 5.6 - - 7 + - 7.0 + - 7.1 - hhvm -script: phpunit --coverage-text tests +script: + - if [[ "$TRAVIS_PHP_VERSION" == '5.6' ]]; then phpunit --coverage-text tests ; fi \ No newline at end of file diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/.vs/config/applicationhost.config b/data/web/inc/lib/vendor/robthree/twofactorauth/.vs/config/applicationhost.config deleted file mode 100644 index 4b9bf477..00000000 --- a/data/web/inc/lib/vendor/robthree/twofactorauth/.vs/config/applicationhost.config +++ /dev/null @@ -1,1031 +0,0 @@ - - - - - - - -
-
-
-
-
-
-
-
- - - -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
- -
-
-
-
-
-
- -
-
-
-
-
- -
-
-
- -
-
- -
-
- -
-
-
- - -
-
-
-
-
-
- -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/README.md b/data/web/inc/lib/vendor/robthree/twofactorauth/README.md index d65eb199..ba6cfeab 100644 --- a/data/web/inc/lib/vendor/robthree/twofactorauth/README.md +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/README.md @@ -10,7 +10,7 @@ PHP library for [two-factor (or multi-factor) authentication](http://en.wikipedi ## Requirements -* Tested on PHP 5.3, 5.4, 5.5 and 5.6, 7 and HHVM +* Tested on PHP 5.3, 5.4, 5.5 and 5.6, 7.0, 7.1 and HHVM * [cURL](http://php.net/manual/en/book.curl.php) when using the provided `GoogleQRCodeProvider` (default), `QRServerProvider` or `QRicketProvider` but you can also provide your own QR-code provider. * [random_bytes()](http://php.net/manual/en/function.random-bytes.php), [MCrypt](http://php.net/manual/en/book.mcrypt.php), [OpenSSL](http://php.net/manual/en/book.openssl.php) or [Hash](http://php.net/manual/en/book.hash.php) depending on which built-in RNG you use (TwoFactorAuth will try to 'autodetect' and use the best available); however: feel free to provide your own (CS)RNG. diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/composer.json b/data/web/inc/lib/vendor/robthree/twofactorauth/composer.json index a4c13758..1ea66ab0 100644 --- a/data/web/inc/lib/vendor/robthree/twofactorauth/composer.json +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/composer.json @@ -1,7 +1,7 @@ { "name": "robthree/twofactorauth", "description": "Two Factor Authentication", - "version": "1.6", + "version": "1.6.1", "type": "library", "keywords": [ "Authentication", "Two Factor Authentication", "Multi Factor Authentication", "TFA", "MFA", "PHP", "Authenticator", "Authy" ], "homepage": "https://github.com/RobThree/TwoFactorAuth", diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Time/ConvertUnixTimeDotComTimeProvider.php b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Time/ConvertUnixTimeDotComTimeProvider.php index 4939f0d4..9a775fc8 100644 --- a/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Time/ConvertUnixTimeDotComTimeProvider.php +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Time/ConvertUnixTimeDotComTimeProvider.php @@ -6,7 +6,7 @@ class ConvertUnixTimeDotComTimeProvider implements ITimeProvider { public function getTime() { $json = @json_decode( - @file_get_contents('http://www.convert-unix-time.com/api?timestamp=now') + @file_get_contents('http://www.convert-unix-time.com/api?timestamp=now&r=' . uniqid(null, true)) ); if ($json === null || !is_int($json->timestamp)) throw new \TimeException('Unable to retrieve time from convert-unix-time.com'); diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Time/HttpTimeProvider.php b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Time/HttpTimeProvider.php index c761bd97..8e7806e2 100644 --- a/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Time/HttpTimeProvider.php +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Time/HttpTimeProvider.php @@ -26,7 +26,8 @@ class HttpTimeProvider implements ITimeProvider 'request_fulluri' => true, 'header' => array( 'Connection: close', - 'User-agent: TwoFactorAuth HttpTimeProvider (https://github.com/RobThree/TwoFactorAuth)' + 'User-agent: TwoFactorAuth HttpTimeProvider (https://github.com/RobThree/TwoFactorAuth)', + 'Cache-Control: no-cache' ) ) ); diff --git a/data/web/inc/lib/vendor/yubico/u2flib-server/src/u2flib_server/U2F.php.1 b/data/web/inc/lib/vendor/yubico/u2flib-server/src/u2flib_server/U2F.php.1 new file mode 100644 index 00000000..a11c78fb --- /dev/null +++ b/data/web/inc/lib/vendor/yubico/u2flib-server/src/u2flib_server/U2F.php.1 @@ -0,0 +1,507 @@ +appId = $appId; + $this->attestDir = $attestDir; + } + + /** + * Called to get a registration request to send to a user. + * Returns an array of one registration request and a array of sign requests. + * + * @param array $registrations List of current registrations for this + * user, to prevent the user from registering the same authenticator several + * times. + * @return array An array of two elements, the first containing a + * RegisterRequest the second being an array of SignRequest + * @throws Error + */ + public function getRegisterData(array $registrations = array()) + { + $challenge = $this->createChallenge(); + $request = new RegisterRequest($challenge, $this->appId); + $signs = $this->getAuthenticateData($registrations); + return array($request, $signs); + } + + /** + * Called to verify and unpack a registration message. + * + * @param RegisterRequest $request this is a reply to + * @param object $response response from a user + * @param bool $includeCert set to true if the attestation certificate should be + * included in the returned Registration object + * @return Registration + * @throws Error + */ + public function doRegister($request, $response, $includeCert = true) + { + if( !is_object( $request ) ) { + throw new \InvalidArgumentException('$request of doRegister() method only accepts object.'); + } + + if( !is_object( $response ) ) { + throw new \InvalidArgumentException('$response of doRegister() method only accepts object.'); + } + + if( property_exists( $response, 'errorCode') && $response->errorCode !== 0 ) { + throw new Error('User-agent returned error. Error code: ' . $response->errorCode, ERR_BAD_UA_RETURNING ); + } + + if( !is_bool( $includeCert ) ) { + throw new \InvalidArgumentException('$include_cert of doRegister() method only accepts boolean.'); + } + + $rawReg = $this->base64u_decode($response->registrationData); + $regData = array_values(unpack('C*', $rawReg)); + $clientData = $this->base64u_decode($response->clientData); + $cli = json_decode($clientData); + + if($cli->challenge !== $request->challenge) { + throw new Error('Registration challenge does not match', ERR_UNMATCHED_CHALLENGE ); + } + + $registration = new Registration(); + $offs = 1; + $pubKey = substr($rawReg, $offs, PUBKEY_LEN); + $offs += PUBKEY_LEN; + // decode the pubKey to make sure it's good + $tmpKey = $this->pubkey_to_pem($pubKey); + if($tmpKey === null) { + throw new Error('Decoding of public key failed', ERR_PUBKEY_DECODE ); + } + $registration->publicKey = base64_encode($pubKey); + $khLen = $regData[$offs++]; + $kh = substr($rawReg, $offs, $khLen); + $offs += $khLen; + $registration->keyHandle = $this->base64u_encode($kh); + + // length of certificate is stored in byte 3 and 4 (excluding the first 4 bytes) + $certLen = 4; + $certLen += ($regData[$offs + 2] << 8); + $certLen += $regData[$offs + 3]; + + $rawCert = $this->fixSignatureUnusedBits(substr($rawReg, $offs, $certLen)); + $offs += $certLen; + $pemCert = "-----BEGIN CERTIFICATE-----\r\n"; + $pemCert .= chunk_split(base64_encode($rawCert), 64); + $pemCert .= "-----END CERTIFICATE-----"; + if($includeCert) { + $registration->certificate = base64_encode($rawCert); + } + if($this->attestDir) { + if(openssl_x509_checkpurpose($pemCert, -1, $this->get_certs()) !== true) { + throw new Error('Attestation certificate can not be validated', ERR_ATTESTATION_VERIFICATION ); + } + } + + if(!openssl_pkey_get_public($pemCert)) { + throw new Error('Decoding of public key failed', ERR_PUBKEY_DECODE ); + } + $signature = substr($rawReg, $offs); + + $dataToVerify = chr(0); + $dataToVerify .= hash('sha256', $request->appId, true); + $dataToVerify .= hash('sha256', $clientData, true); + $dataToVerify .= $kh; + $dataToVerify .= $pubKey; + + if(openssl_verify($dataToVerify, $signature, $pemCert, 'sha256') === 1) { + return $registration; + } else { + throw new Error('Attestation signature does not match', ERR_ATTESTATION_SIGNATURE ); + } + } + + /** + * Called to get an authentication request. + * + * @param array $registrations An array of the registrations to create authentication requests for. + * @return array An array of SignRequest + * @throws Error + */ + public function getAuthenticateData(array $registrations) + { + $sigs = array(); + $challenge = $this->createChallenge(); + foreach ($registrations as $reg) { + if( !is_object( $reg ) ) { + throw new \InvalidArgumentException('$registrations of getAuthenticateData() method only accepts array of object.'); + } + + $sig = new SignRequest(); + $sig->appId = $this->appId; + $sig->keyHandle = $reg->keyHandle; + $sig->challenge = $challenge; + $sigs[] = $sig; + } + return $sigs; + } + + /** + * Called to verify an authentication response + * + * @param array $requests An array of outstanding authentication requests + * @param array $registrations An array of current registrations + * @param object $response A response from the authenticator + * @return Registration + * @throws Error + * + * The Registration object returned on success contains an updated counter + * that should be saved for future authentications. + * If the Error returned is ERR_COUNTER_TOO_LOW this is an indication of + * token cloning or similar and appropriate action should be taken. + */ + public function doAuthenticate(array $requests, array $registrations, $response) + { + if( !is_object( $response ) ) { + throw new \InvalidArgumentException('$response of doAuthenticate() method only accepts object.'); + } + + if( property_exists( $response, 'errorCode') && $response->errorCode !== 0 ) { + throw new Error('User-agent returned error. Error code: ' . $response->errorCode, ERR_BAD_UA_RETURNING ); + } + + /** @var object|null $req */ + $req = null; + + /** @var object|null $reg */ + $reg = null; + + $clientData = $this->base64u_decode($response->clientData); + $decodedClient = json_decode($clientData); + foreach ($requests as $req) { + if( !is_object( $req ) ) { + throw new \InvalidArgumentException('$requests of doAuthenticate() method only accepts array of object.'); + } + + if($req->keyHandle === $response->keyHandle && $req->challenge === $decodedClient->challenge) { + break; + } + + $req = null; + } + if($req === null) { + throw new Error('No matching request found', ERR_NO_MATCHING_REQUEST ); + } + foreach ($registrations as $reg) { + if( !is_object( $reg ) ) { + throw new \InvalidArgumentException('$registrations of doAuthenticate() method only accepts array of object.'); + } + + if($reg->keyHandle === $response->keyHandle) { + break; + } + $reg = null; + } + if($reg === null) { + throw new Error('No matching registration found', ERR_NO_MATCHING_REGISTRATION ); + } + $pemKey = $this->pubkey_to_pem($this->base64u_decode($reg->publicKey)); + if($pemKey === null) { + throw new Error('Decoding of public key failed', ERR_PUBKEY_DECODE ); + } + + $signData = $this->base64u_decode($response->signatureData); + $dataToVerify = hash('sha256', $req->appId, true); + $dataToVerify .= substr($signData, 0, 5); + $dataToVerify .= hash('sha256', $clientData, true); + $signature = substr($signData, 5); + + if(openssl_verify($dataToVerify, $signature, $pemKey, 'sha256') === 1) { + $ctr = unpack("Nctr", substr($signData, 1, 4)); + $counter = $ctr['ctr']; + /* TODO: wrap-around should be handled somehow.. */ + if($counter > $reg->counter) { + $reg->counter = $counter; + return $reg; + } else { + throw new Error('Counter too low.', ERR_COUNTER_TOO_LOW ); + } + } else { + throw new Error('Authentication failed', ERR_AUTHENTICATION_FAILURE ); + } + } + + /** + * @return array + */ + private function get_certs() + { + $files = array(); + $dir = $this->attestDir; + if($dir && $handle = opendir($dir)) { + while(false !== ($entry = readdir($handle))) { + if(is_file("$dir/$entry")) { + $files[] = "$dir/$entry"; + } + } + closedir($handle); + } + return $files; + } + + /** + * @param string $data + * @return string + */ + private function base64u_encode($data) + { + return trim(strtr(base64_encode($data), '+/', '-_'), '='); + } + + /** + * @param string $data + * @return string + */ + private function base64u_decode($data) + { + return base64_decode(strtr($data, '-_', '+/')); + } + + /** + * @param string $key + * @return null|string + */ + private function pubkey_to_pem($key) + { + if(strlen($key) !== PUBKEY_LEN || $key[0] !== "\x04") { + return null; + } + + /* + * Convert the public key to binary DER format first + * Using the ECC SubjectPublicKeyInfo OIDs from RFC 5480 + * + * SEQUENCE(2 elem) 30 59 + * SEQUENCE(2 elem) 30 13 + * OID1.2.840.10045.2.1 (id-ecPublicKey) 06 07 2a 86 48 ce 3d 02 01 + * OID1.2.840.10045.3.1.7 (secp256r1) 06 08 2a 86 48 ce 3d 03 01 07 + * BIT STRING(520 bit) 03 42 ..key.. + */ + $der = "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01"; + $der .= "\x06\x08\x2a\x86\x48\xce\x3d\x03\x01\x07\x03\x42"; + $der .= "\0".$key; + + $pem = "-----BEGIN PUBLIC KEY-----\r\n"; + $pem .= chunk_split(base64_encode($der), 64); + $pem .= "-----END PUBLIC KEY-----"; + + return $pem; + } + + /** + * @return string + * @throws Error + */ + private function createChallenge() + { + $challenge = openssl_random_pseudo_bytes(32, $crypto_strong ); + if( $crypto_strong !== true ) { + throw new Error('Unable to obtain a good source of randomness', ERR_BAD_RANDOM); + } + + $challenge = $this->base64u_encode( $challenge ); + + return $challenge; + } + + /** + * Fixes a certificate where the signature contains unused bits. + * + * @param string $cert + * @return mixed + */ + private function fixSignatureUnusedBits($cert) + { + if(in_array(hash('sha256', $cert), $this->FIXCERTS)) { + $cert[strlen($cert) - 257] = "\0"; + } + return $cert; + } +} + +/** + * Class for building a registration request + * + * @package u2flib_server + */ +class RegisterRequest +{ + /** Protocol version */ + public $version = U2F_VERSION; + + /** Registration challenge */ + public $challenge; + + /** Application id */ + public $appId; + + /** + * @param string $challenge + * @param string $appId + * @internal + */ + public function __construct($challenge, $appId) + { + $this->challenge = $challenge; + $this->appId = $appId; + } +} + +/** + * Class for building up an authentication request + * + * @package u2flib_server + */ +class SignRequest +{ + /** Protocol version */ + public $version = U2F_VERSION; + + /** Authentication challenge */ + public $challenge; + + /** Key handle of a registered authenticator */ + public $keyHandle; + + /** Application id */ + public $appId; +} + +/** + * Class returned for successful registrations + * + * @package u2flib_server + */ +class Registration +{ + /** The key handle of the registered authenticator */ + public $keyHandle; + + /** The public key of the registered authenticator */ + public $publicKey; + + /** The attestation certificate of the registered authenticator */ + public $certificate; + + /** The counter associated with this registration */ + public $counter = -1; +} + +/** + * Error class, returned on errors + * + * @package u2flib_server + */ +class Error extends \Exception +{ + /** + * Override constructor and make message and code mandatory + * @param string $message + * @param int $code + * @param \Exception|null $previous + */ + public function __construct($message, $code, \Exception $previous = null) { + parent::__construct($message, $code, $previous); + } +} diff --git a/data/web/index.php b/data/web/index.php index 6c5ba3c7..42087cdc 100644 --- a/data/web/index.php +++ b/data/web/index.php @@ -73,12 +73,14 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI']; $val): - ?> -   - $val): + ?> +   + diff --git a/data/web/js/u2f-api.js b/data/web/js/u2f-api.js index 0f06f50d..9244d14e 100644 --- a/data/web/js/u2f-api.js +++ b/data/web/js/u2f-api.js @@ -1,26 +1,40 @@ -// Copyright 2014-2015 Google Inc. All rights reserved. -// -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file or at -// https://developers.google.com/open-source/licenses/bsd +//Copyright 2014-2015 Google Inc. All rights reserved. + +//Use of this source code is governed by a BSD-style +//license that can be found in the LICENSE file or at +//https://developers.google.com/open-source/licenses/bsd /** * @fileoverview The U2F api. */ - 'use strict'; -/** Namespace for the U2F api. + +/** + * Namespace for the U2F api. * @type {Object} */ var u2f = u2f || {}; /** - * The U2F extension id - * @type {string} - * @const + * FIDO U2F Javascript API Version + * @number */ -u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd'; +var js_api_version; + +/** + * The U2F extension id + * @const {string} + */ +// The Chrome packaged app extension ID. +// Uncomment this if you want to deploy a server instance that uses +// the package Chrome app and does not require installing the U2F Chrome extension. + u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd'; +// The U2F Chrome extension ID. +// Uncomment this if you want to deploy a server instance that uses +// the U2F Chrome extension to authenticate. +// u2f.EXTENSION_ID = 'pfboblefjcgdjicmnffhdgionmgcdmne'; + /** * Message types for messsages to/from the extension @@ -29,11 +43,14 @@ u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd'; */ u2f.MessageTypes = { 'U2F_REGISTER_REQUEST': 'u2f_register_request', - 'U2F_SIGN_REQUEST': 'u2f_sign_request', 'U2F_REGISTER_RESPONSE': 'u2f_register_response', - 'U2F_SIGN_RESPONSE': 'u2f_sign_response' + 'U2F_SIGN_REQUEST': 'u2f_sign_request', + 'U2F_SIGN_RESPONSE': 'u2f_sign_response', + 'U2F_GET_API_VERSION_REQUEST': 'u2f_get_api_version_request', + 'U2F_GET_API_VERSION_RESPONSE': 'u2f_get_api_version_response' }; + /** * Response status codes * @const @@ -48,17 +65,18 @@ u2f.ErrorCodes = { 'TIMEOUT': 5 }; + /** - * A message type for registration requests + * A message for registration requests * @typedef {{ * type: u2f.MessageTypes, - * signRequests: Array, - * registerRequests: ?Array, + * appId: ?string, * timeoutSeconds: ?number, * requestId: ?number * }} */ -u2f.Request; +u2f.U2fRequest; + /** * A message for registration responses @@ -68,7 +86,8 @@ u2f.Request; * requestId: ?number * }} */ -u2f.Response; +u2f.U2fResponse; + /** * An error object for responses @@ -79,6 +98,19 @@ u2f.Response; */ u2f.Error; +/** + * Data object for a single sign request. + * @typedef {enum {BLUETOOTH_RADIO, BLUETOOTH_LOW_ENERGY, USB, NFC}} + */ +u2f.Transport; + + +/** + * Data object for a single sign request. + * @typedef {Array} + */ +u2f.Transports; + /** * Data object for a single sign request. * @typedef {{ @@ -90,6 +122,7 @@ u2f.Error; */ u2f.SignRequest; + /** * Data object for a sign response. * @typedef {{ @@ -100,27 +133,51 @@ u2f.SignRequest; */ u2f.SignResponse; + /** * Data object for a registration request. * @typedef {{ * version: string, - * challenge: string, - * appId: string + * challenge: string * }} */ u2f.RegisterRequest; + /** * Data object for a registration response. * @typedef {{ - * registrationData: string, - * clientData: string + * version: string, + * keyHandle: string, + * transports: Transports, + * appId: string * }} */ u2f.RegisterResponse; -// Low level MessagePort API support +/** + * Data object for a registered key. + * @typedef {{ + * version: string, + * keyHandle: string, + * transports: ?Transports, + * appId: ?string + * }} + */ +u2f.RegisteredKey; + + +/** + * Data object for a get API register response. + * @typedef {{ + * js_api_version: number + * }} + */ +u2f.GetJsApiVersionResponse; + + +//Low level MessagePort API support /** * Sets up a MessagePort to the U2F extension using the @@ -128,32 +185,34 @@ u2f.RegisterResponse; * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback */ u2f.getMessagePort = function(callback) { - if (typeof chrome != 'undefined' && chrome.runtime) { - // The actual message here does not matter, but we need to get a reply - // for the callback to run. Thus, send an empty signature request - // in order to get a failure response. - var msg = { - type: u2f.MessageTypes.U2F_SIGN_REQUEST, - signRequests: [] - }; - chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function() { - if (!chrome.runtime.lastError) { - // We are on a whitelisted origin and can talk directly - // with the extension. - u2f.getChromeRuntimePort_(callback); - } else { - // chrome.runtime was available, but we couldn't message - // the extension directly, use iframe - u2f.getIframePort_(callback); - } - }); - } else if (u2f.isAndroidChrome_()) { - u2f.getAuthenticatorPort_(callback); - } else { - // chrome.runtime was not available at all, which is normal - // when this origin doesn't have access to any extensions. + if (typeof chrome != 'undefined' && chrome.runtime) { + // The actual message here does not matter, but we need to get a reply + // for the callback to run. Thus, send an empty signature request + // in order to get a failure response. + var msg = { + type: u2f.MessageTypes.U2F_SIGN_REQUEST, + signRequests: [] + }; + chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function() { + if (!chrome.runtime.lastError) { + // We are on a whitelisted origin and can talk directly + // with the extension. + u2f.getChromeRuntimePort_(callback); + } else { + // chrome.runtime was available, but we couldn't message + // the extension directly, use iframe u2f.getIframePort_(callback); - } + } + }); + } else if (u2f.isAndroidChrome_()) { + u2f.getAuthenticatorPort_(callback); + } else if (u2f.isIosChrome_()) { + u2f.getIosPort_(callback); + } else { + // chrome.runtime was not available at all, which is normal + // when this origin doesn't have access to any extensions. + u2f.getIframePort_(callback); + } }; /** @@ -161,22 +220,30 @@ u2f.getMessagePort = function(callback) { * @private */ u2f.isAndroidChrome_ = function() { - var userAgent = navigator.userAgent; - return userAgent.indexOf('Chrome') != -1 && - userAgent.indexOf('Android') != -1; + var userAgent = navigator.userAgent; + return userAgent.indexOf('Chrome') != -1 && + userAgent.indexOf('Android') != -1; }; /** - * Connects directly to the extension via chrome.runtime.connect + * Detect chrome running on iOS based on the browser's platform. + * @private + */ +u2f.isIosChrome_ = function() { + return ["iPhone", "iPad", "iPod"].indexOf(navigator.platform) > -1; +}; + +/** + * Connects directly to the extension via chrome.runtime.connect. * @param {function(u2f.WrappedChromeRuntimePort_)} callback * @private */ u2f.getChromeRuntimePort_ = function(callback) { - var port = chrome.runtime.connect(u2f.EXTENSION_ID, - {'includeTlsChannelId': true}); - setTimeout(function() { - callback(new u2f.WrappedChromeRuntimePort_(port)); - }, 0); + var port = chrome.runtime.connect(u2f.EXTENSION_ID, + {'includeTlsChannelId': true}); + setTimeout(function() { + callback(new u2f.WrappedChromeRuntimePort_(port)); + }, 0); }; /** @@ -185,9 +252,20 @@ u2f.getChromeRuntimePort_ = function(callback) { * @private */ u2f.getAuthenticatorPort_ = function(callback) { - setTimeout(function() { - callback(new u2f.WrappedAuthenticatorPort_()); - }, 0); + setTimeout(function() { + callback(new u2f.WrappedAuthenticatorPort_()); + }, 0); +}; + +/** + * Return a 'port' abstraction to the iOS client app. + * @param {function(u2f.WrappedIosPort_)} callback + * @private + */ +u2f.getIosPort_ = function(callback) { + setTimeout(function() { + callback(new u2f.WrappedIosPort_()); + }, 0); }; /** @@ -197,53 +275,100 @@ u2f.getAuthenticatorPort_ = function(callback) { * @private */ u2f.WrappedChromeRuntimePort_ = function(port) { - this.port_ = port; + this.port_ = port; }; /** - * Format a return a sign request. + * Format and return a sign request compliant with the JS API version supported by the extension. * @param {Array} signRequests * @param {number} timeoutSeconds * @param {number} reqId * @return {Object} */ -u2f.WrappedChromeRuntimePort_.prototype.formatSignRequest_ = - function(signRequests, timeoutSeconds, reqId) { - return { - type: u2f.MessageTypes.U2F_SIGN_REQUEST, - signRequests: signRequests, - timeoutSeconds: timeoutSeconds, - requestId: reqId - }; +u2f.formatSignRequest_ = + function(appId, challenge, registeredKeys, timeoutSeconds, reqId) { + if (js_api_version === undefined || js_api_version < 1.1) { + // Adapt request to the 1.0 JS API + var signRequests = []; + for (var i = 0; i < registeredKeys.length; i++) { + signRequests[i] = { + version: registeredKeys[i].version, + challenge: challenge, + keyHandle: registeredKeys[i].keyHandle, + appId: appId + }; + } + return { + type: u2f.MessageTypes.U2F_SIGN_REQUEST, + signRequests: signRequests, + timeoutSeconds: timeoutSeconds, + requestId: reqId }; + } + // JS 1.1 API + return { + type: u2f.MessageTypes.U2F_SIGN_REQUEST, + appId: appId, + challenge: challenge, + registeredKeys: registeredKeys, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; +}; /** - * Format a return a register request. + * Format and return a register request compliant with the JS API version supported by the extension.. * @param {Array} signRequests * @param {Array} signRequests * @param {number} timeoutSeconds * @param {number} reqId * @return {Object} */ -u2f.WrappedChromeRuntimePort_.prototype.formatRegisterRequest_ = - function(signRequests, registerRequests, timeoutSeconds, reqId) { - return { - type: u2f.MessageTypes.U2F_REGISTER_REQUEST, - signRequests: signRequests, - registerRequests: registerRequests, - timeoutSeconds: timeoutSeconds, - requestId: reqId - }; +u2f.formatRegisterRequest_ = + function(appId, registeredKeys, registerRequests, timeoutSeconds, reqId) { + if (js_api_version === undefined || js_api_version < 1.1) { + // Adapt request to the 1.0 JS API + for (var i = 0; i < registerRequests.length; i++) { + registerRequests[i].appId = appId; + } + var signRequests = []; + for (var i = 0; i < registeredKeys.length; i++) { + signRequests[i] = { + version: registeredKeys[i].version, + challenge: registerRequests[0], + keyHandle: registeredKeys[i].keyHandle, + appId: appId + }; + } + return { + type: u2f.MessageTypes.U2F_REGISTER_REQUEST, + signRequests: signRequests, + registerRequests: registerRequests, + timeoutSeconds: timeoutSeconds, + requestId: reqId }; + } + // JS 1.1 API + return { + type: u2f.MessageTypes.U2F_REGISTER_REQUEST, + appId: appId, + registerRequests: registerRequests, + registeredKeys: registeredKeys, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; +}; + /** * Posts a message on the underlying channel. * @param {Object} message */ u2f.WrappedChromeRuntimePort_.prototype.postMessage = function(message) { - this.port_.postMessage(message); + this.port_.postMessage(message); }; + /** * Emulates the HTML 5 addEventListener interface. Works only for the * onmessage event, which is hooked up to the chrome.runtime.Port.onMessage. @@ -252,16 +377,16 @@ u2f.WrappedChromeRuntimePort_.prototype.postMessage = function(message) { */ u2f.WrappedChromeRuntimePort_.prototype.addEventListener = function(eventName, handler) { - var name = eventName.toLowerCase(); - if (name == 'message' || name == 'onmessage') { - this.port_.onMessage.addListener(function(message) { - // Emulate a minimal MessageEvent object - handler({'data': message}); - }); - } else { - console.error('WrappedChromeRuntimePort only supports onMessage'); - } - }; + var name = eventName.toLowerCase(); + if (name == 'message' || name == 'onmessage') { + this.port_.onMessage.addListener(function(message) { + // Emulate a minimal MessageEvent object + handler({'data': message}); + }); + } else { + console.error('WrappedChromeRuntimePort only supports onMessage'); + } +}; /** * Wrap the Authenticator app with a MessagePort interface. @@ -269,8 +394,8 @@ u2f.WrappedChromeRuntimePort_.prototype.addEventListener = * @private */ u2f.WrappedAuthenticatorPort_ = function() { - this.requestId_ = -1; - this.requestObject_ = null; + this.requestId_ = -1; + this.requestObject_ = null; } /** @@ -278,28 +403,39 @@ u2f.WrappedAuthenticatorPort_ = function() { * @param {Object} message */ u2f.WrappedAuthenticatorPort_.prototype.postMessage = function(message) { - var intentLocation = /** @type {string} */ (message); - document.location = intentLocation; + var intentUrl = + u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ + + ';S.request=' + encodeURIComponent(JSON.stringify(message)) + + ';end'; + document.location = intentUrl; }; +/** + * Tells what type of port this is. + * @return {String} port type + */ +u2f.WrappedAuthenticatorPort_.prototype.getPortType = function() { + return "WrappedAuthenticatorPort_"; +}; + + /** * Emulates the HTML 5 addEventListener interface. * @param {string} eventName * @param {function({data: Object})} handler */ -u2f.WrappedAuthenticatorPort_.prototype.addEventListener = - function(eventName, handler) { - var name = eventName.toLowerCase(); - if (name == 'message') { - var self = this; - /* Register a callback to that executes when - * chrome injects the response. */ - window.addEventListener( - 'message', self.onRequestUpdate_.bind(self, handler), false); - } else { - console.error('WrappedAuthenticatorPort only supports message'); - } - }; +u2f.WrappedAuthenticatorPort_.prototype.addEventListener = function(eventName, handler) { + var name = eventName.toLowerCase(); + if (name == 'message') { + var self = this; + /* Register a callback to that executes when + * chrome injects the response. */ + window.addEventListener( + 'message', self.onRequestUpdate_.bind(self, handler), false); + } else { + console.error('WrappedAuthenticatorPort only supports message'); + } +}; /** * Callback invoked when a response is received from the Authenticator. @@ -308,72 +444,18 @@ u2f.WrappedAuthenticatorPort_.prototype.addEventListener = */ u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ = function(callback, message) { - var messageObject = JSON.parse(message.data); - var intentUrl = messageObject['intentURL']; + var messageObject = JSON.parse(message.data); + var intentUrl = messageObject['intentURL']; - var errorCode = messageObject['errorCode']; - var responseObject = null; - if (messageObject.hasOwnProperty('data')) { - responseObject = /** @type {Object} */ ( - JSON.parse(messageObject['data'])); - responseObject['requestId'] = this.requestId_; - } + var errorCode = messageObject['errorCode']; + var responseObject = null; + if (messageObject.hasOwnProperty('data')) { + responseObject = /** @type {Object} */ ( + JSON.parse(messageObject['data'])); + } - /* Sign responses from the authenticator do not conform to U2F, - * convert to U2F here. */ - responseObject = this.doResponseFixups_(responseObject); - callback({'data': responseObject}); - }; - -/** - * Fixup the response provided by the Authenticator to conform with - * the U2F spec. - * @param {Object} responseData - * @return {Object} the U2F compliant response object - */ -u2f.WrappedAuthenticatorPort_.prototype.doResponseFixups_ = - function(responseObject) { - if (responseObject.hasOwnProperty('responseData')) { - return responseObject; - } else if (this.requestObject_['type'] != u2f.MessageTypes.U2F_SIGN_REQUEST) { - // Only sign responses require fixups. If this is not a response - // to a sign request, then an internal error has occurred. - return { - 'type': u2f.MessageTypes.U2F_REGISTER_RESPONSE, - 'responseData': { - 'errorCode': u2f.ErrorCodes.OTHER_ERROR, - 'errorMessage': 'Internal error: invalid response from Authenticator' - } - }; - } - - /* Non-conformant sign response, do fixups. */ - var encodedChallengeObject = responseObject['challenge']; - if (typeof encodedChallengeObject !== 'undefined') { - var challengeObject = JSON.parse(atob(encodedChallengeObject)); - var serverChallenge = challengeObject['challenge']; - var challengesList = this.requestObject_['signData']; - var requestChallengeObject = null; - for (var i = 0; i < challengesList.length; i++) { - var challengeObject = challengesList[i]; - if (challengeObject['keyHandle'] == responseObject['keyHandle']) { - requestChallengeObject = challengeObject; - break; - } - } - } - var responseData = { - 'errorCode': responseObject['resultCode'], - 'keyHandle': responseObject['keyHandle'], - 'signatureData': responseObject['signature'], - 'clientData': encodedChallengeObject - }; - return { - 'type': u2f.MessageTypes.U2F_SIGN_RESPONSE, - 'responseData': responseData, - 'requestId': responseObject['requestId'] - } - }; + callback({'data': responseObject}); +}; /** * Base URL for intents to Authenticator. @@ -381,126 +463,44 @@ u2f.WrappedAuthenticatorPort_.prototype.doResponseFixups_ = * @private */ u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ = - 'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE'; + 'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE'; /** - * Format a return a sign request. - * @param {Array} signRequests - * @param {number} timeoutSeconds (ignored for now) - * @param {number} reqId - * @return {string} - */ -u2f.WrappedAuthenticatorPort_.prototype.formatSignRequest_ = - function(signRequests, timeoutSeconds, reqId) { - if (!signRequests || signRequests.length == 0) { - return null; - } - /* TODO(fixme): stash away requestId, as the authenticator app does - * not return it for sign responses. */ - this.requestId_ = reqId; - /* TODO(fixme): stash away the signRequests, to deal with the legacy - * response format returned by the Authenticator app. */ - this.requestObject_ = { - 'type': u2f.MessageTypes.U2F_SIGN_REQUEST, - 'signData': signRequests, - 'requestId': reqId, - 'timeout': timeoutSeconds - }; - - var appId = signRequests[0]['appId']; - var intentUrl = - u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ + - ';S.appId=' + encodeURIComponent(appId) + - ';S.eventId=' + reqId + - ';S.challenges=' + - encodeURIComponent( - JSON.stringify(this.getBrowserDataList_(signRequests))) + ';end'; - return intentUrl; - }; - -/** - * Get the browser data objects from the challenge list - * @param {Array} challenges list of challenges - * @return {Array} list of browser data objects + * Wrap the iOS client app with a MessagePort interface. + * @constructor * @private */ -u2f.WrappedAuthenticatorPort_ - .prototype.getBrowserDataList_ = function(challenges) { - return challenges - .map(function(challenge) { - var browserData = { - 'typ': 'navigator.id.getAssertion', - 'challenge': challenge['challenge'] - }; - var challengeObject = { - 'challenge' : browserData, - 'keyHandle' : challenge['keyHandle'] - }; - return challengeObject; - }); +u2f.WrappedIosPort_ = function() {}; + +/** + * Launch the iOS client app request + * @param {Object} message + */ +u2f.WrappedIosPort_.prototype.postMessage = function(message) { + var str = JSON.stringify(message); + var url = "u2f://auth?" + encodeURI(str); + location.replace(url); }; /** - * Format a return a register request. - * @param {Array} signRequests - * @param {Array} enrollChallenges - * @param {number} timeoutSeconds (ignored for now) - * @param {number} reqId - * @return {Object} + * Tells what type of port this is. + * @return {String} port type */ -u2f.WrappedAuthenticatorPort_.prototype.formatRegisterRequest_ = - function(signRequests, enrollChallenges, timeoutSeconds, reqId) { - if (!enrollChallenges || enrollChallenges.length == 0) { - return null; - } - // Assume the appId is the same for all enroll challenges. - var appId = enrollChallenges[0]['appId']; - var registerRequests = []; - for (var i = 0; i < enrollChallenges.length; i++) { - var registerRequest = { - 'challenge': enrollChallenges[i]['challenge'], - 'version': enrollChallenges[i]['version'] - }; - if (enrollChallenges[i]['appId'] != appId) { - // Only include the appId when it differs from the first appId. - registerRequest['appId'] = enrollChallenges[i]['appId']; - } - registerRequests.push(registerRequest); - } - var registeredKeys = []; - if (signRequests) { - for (i = 0; i < signRequests.length; i++) { - var key = { - 'keyHandle': signRequests[i]['keyHandle'], - 'version': signRequests[i]['version'] - }; - // Only include the appId when it differs from the appId that's - // being registered now. - if (signRequests[i]['appId'] != appId) { - key['appId'] = signRequests[i]['appId']; - } - registeredKeys.push(key); - } - } - var request = { - 'type': u2f.MessageTypes.U2F_REGISTER_REQUEST, - 'appId': appId, - 'registerRequests': registerRequests, - 'registeredKeys': registeredKeys, - 'requestId': reqId, - 'timeoutSeconds': timeoutSeconds - }; - var intentUrl = - u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ + - ';S.request=' + encodeURIComponent(JSON.stringify(request)) + - ';end'; - /* TODO(fixme): stash away requestId, this is is not necessary for - * register requests, but here to keep parity with sign. - */ - this.requestId_ = reqId; - return intentUrl; - }; +u2f.WrappedIosPort_.prototype.getPortType = function() { + return "WrappedIosPort_"; +}; +/** + * Emulates the HTML 5 addEventListener interface. + * @param {string} eventName + * @param {function({data: Object})} handler + */ +u2f.WrappedIosPort_.prototype.addEventListener = function(eventName, handler) { + var name = eventName.toLowerCase(); + if (name !== 'message') { + console.error('WrappedIosPort only supports message'); + } +}; /** * Sets up an embedded trampoline iframe, sourced from the extension. @@ -508,33 +508,33 @@ u2f.WrappedAuthenticatorPort_.prototype.formatRegisterRequest_ = * @private */ u2f.getIframePort_ = function(callback) { - // Create the iframe - var iframeOrigin = 'chrome-extension://' + u2f.EXTENSION_ID; - var iframe = document.createElement('iframe'); - iframe.src = iframeOrigin + '/u2f-comms.html'; - iframe.setAttribute('style', 'display:none'); - document.body.appendChild(iframe); + // Create the iframe + var iframeOrigin = 'chrome-extension://' + u2f.EXTENSION_ID; + var iframe = document.createElement('iframe'); + iframe.src = iframeOrigin + '/u2f-comms.html'; + iframe.setAttribute('style', 'display:none'); + document.body.appendChild(iframe); - var channel = new MessageChannel(); - var ready = function(message) { - if (message.data == 'ready') { - channel.port1.removeEventListener('message', ready); - callback(channel.port1); - } else { - console.error('First event on iframe port was not "ready"'); - } - }; - channel.port1.addEventListener('message', ready); - channel.port1.start(); + var channel = new MessageChannel(); + var ready = function(message) { + if (message.data == 'ready') { + channel.port1.removeEventListener('message', ready); + callback(channel.port1); + } else { + console.error('First event on iframe port was not "ready"'); + } + }; + channel.port1.addEventListener('message', ready); + channel.port1.start(); - iframe.addEventListener('load', function() { - // Deliver the port to the iframe and initialize - iframe.contentWindow.postMessage('init', iframeOrigin, [channel.port2]); - }); + iframe.addEventListener('load', function() { + // Deliver the port to the iframe and initialize + iframe.contentWindow.postMessage('init', iframeOrigin, [channel.port2]); + }); }; -// High-level JS API +//High-level JS API /** * Default extension response timeout in seconds. @@ -577,22 +577,22 @@ u2f.callbackMap_ = {}; * @private */ u2f.getPortSingleton_ = function(callback) { - if (u2f.port_) { - callback(u2f.port_); - } else { - if (u2f.waitingForPort_.length == 0) { - u2f.getMessagePort(function(port) { - u2f.port_ = port; - u2f.port_.addEventListener('message', - /** @type {function(Event)} */ (u2f.responseHandler_)); + if (u2f.port_) { + callback(u2f.port_); + } else { + if (u2f.waitingForPort_.length == 0) { + u2f.getMessagePort(function(port) { + u2f.port_ = port; + u2f.port_.addEventListener('message', + /** @type {function(Event)} */ (u2f.responseHandler_)); - // Careful, here be async callbacks. Maybe. - while (u2f.waitingForPort_.length) - u2f.waitingForPort_.shift()(u2f.port_); - }); - } - u2f.waitingForPort_.push(callback); + // Careful, here be async callbacks. Maybe. + while (u2f.waitingForPort_.length) + u2f.waitingForPort_.shift()(u2f.port_); + }); } + u2f.waitingForPort_.push(callback); + } }; /** @@ -601,51 +601,148 @@ u2f.getPortSingleton_ = function(callback) { * @private */ u2f.responseHandler_ = function(message) { - var response = message.data; - var reqId = response['requestId']; - if (!reqId || !u2f.callbackMap_[reqId]) { - console.error('Unknown or missing requestId in response.'); - return; - } - var cb = u2f.callbackMap_[reqId]; - delete u2f.callbackMap_[reqId]; - cb(response['responseData']); + var response = message.data; + var reqId = response['requestId']; + if (!reqId || !u2f.callbackMap_[reqId]) { + console.error('Unknown or missing requestId in response.'); + return; + } + var cb = u2f.callbackMap_[reqId]; + delete u2f.callbackMap_[reqId]; + cb(response['responseData']); }; /** * Dispatches an array of sign requests to available U2F tokens. - * @param {Array} signRequests + * If the JS API version supported by the extension is unknown, it first sends a + * message to the extension to find out the supported API version and then it sends + * the sign request. + * @param {string=} appId + * @param {string=} challenge + * @param {Array} registeredKeys * @param {function((u2f.Error|u2f.SignResponse))} callback * @param {number=} opt_timeoutSeconds */ -u2f.sign = function(signRequests, callback, opt_timeoutSeconds) { - u2f.getPortSingleton_(function(port) { - var reqId = ++u2f.reqCounter_; - u2f.callbackMap_[reqId] = callback; - var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? - opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); - var req = port.formatSignRequest_(signRequests, timeoutSeconds, reqId); - port.postMessage(req); - }); +u2f.sign = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) { + if (js_api_version === undefined) { + // Send a message to get the extension to JS API version, then send the actual sign request. + u2f.getApiVersion( + function (response) { + js_api_version = response['js_api_version'] === undefined ? 0 : response['js_api_version']; + console.log("Extension JS API Version: ", js_api_version); + u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds); + }); + } else { + // We know the JS API version. Send the actual sign request in the supported API version. + u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds); + } +}; + +/** + * Dispatches an array of sign requests to available U2F tokens. + * @param {string=} appId + * @param {string=} challenge + * @param {Array} registeredKeys + * @param {function((u2f.Error|u2f.SignResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.sendSignRequest = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) { + u2f.getPortSingleton_(function(port) { + var reqId = ++u2f.reqCounter_; + u2f.callbackMap_[reqId] = callback; + var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? + opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); + var req = u2f.formatSignRequest_(appId, challenge, registeredKeys, timeoutSeconds, reqId); + port.postMessage(req); + }); }; /** * Dispatches register requests to available U2F tokens. An array of sign * requests identifies already registered tokens. + * If the JS API version supported by the extension is unknown, it first sends a + * message to the extension to find out the supported API version and then it sends + * the register request. + * @param {string=} appId * @param {Array} registerRequests - * @param {Array} signRequests + * @param {Array} registeredKeys * @param {function((u2f.Error|u2f.RegisterResponse))} callback * @param {number=} opt_timeoutSeconds */ -u2f.register = function(registerRequests, signRequests, - callback, opt_timeoutSeconds) { - u2f.getPortSingleton_(function(port) { - var reqId = ++u2f.reqCounter_; - u2f.callbackMap_[reqId] = callback; - var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? - opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); - var req = port.formatRegisterRequest_( - signRequests, registerRequests, timeoutSeconds, reqId); - port.postMessage(req); - }); -}; \ No newline at end of file +u2f.register = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) { + if (js_api_version === undefined) { + // Send a message to get the extension to JS API version, then send the actual register request. + u2f.getApiVersion( + function (response) { + js_api_version = response['js_api_version'] === undefined ? 0: response['js_api_version']; + console.log("Extension JS API Version: ", js_api_version); + u2f.sendRegisterRequest(appId, registerRequests, registeredKeys, + callback, opt_timeoutSeconds); + }); + } else { + // We know the JS API version. Send the actual register request in the supported API version. + u2f.sendRegisterRequest(appId, registerRequests, registeredKeys, + callback, opt_timeoutSeconds); + } +}; + +/** + * Dispatches register requests to available U2F tokens. An array of sign + * requests identifies already registered tokens. + * @param {string=} appId + * @param {Array} registerRequests + * @param {Array} registeredKeys + * @param {function((u2f.Error|u2f.RegisterResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.sendRegisterRequest = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) { + u2f.getPortSingleton_(function(port) { + var reqId = ++u2f.reqCounter_; + u2f.callbackMap_[reqId] = callback; + var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? + opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); + var req = u2f.formatRegisterRequest_( + appId, registeredKeys, registerRequests, timeoutSeconds, reqId); + port.postMessage(req); + }); +}; + + +/** + * Dispatches a message to the extension to find out the supported + * JS API version. + * If the user is on a mobile phone and is thus using Google Authenticator instead + * of the Chrome extension, don't send the request and simply return 0. + * @param {function((u2f.Error|u2f.GetJsApiVersionResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.getApiVersion = function(callback, opt_timeoutSeconds) { + u2f.getPortSingleton_(function(port) { + // If we are using Android Google Authenticator or iOS client app, + // do not fire an intent to ask which JS API version to use. + if (port.getPortType) { + var apiVersion; + switch (port.getPortType()) { + case 'WrappedIosPort_': + case 'WrappedAuthenticatorPort_': + apiVersion = 1.1; + break; + + default: + apiVersion = 0; + break; + } + callback({ 'js_api_version': apiVersion }); + return; + } + var reqId = ++u2f.reqCounter_; + u2f.callbackMap_[reqId] = callback; + var req = { + type: u2f.MessageTypes.U2F_GET_API_VERSION_REQUEST, + timeoutSeconds: (typeof opt_timeoutSeconds !== 'undefined' ? + opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC), + requestId: reqId + }; + port.postMessage(req); + }); +}; diff --git a/data/web/json_api.php b/data/web/json_api.php index 9ada1044..cf6d8416 100644 --- a/data/web/json_api.php +++ b/data/web/json_api.php @@ -1342,10 +1342,13 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u case "u2f-registration": header('Content-Type: application/javascript'); if (($_SESSION["mailcow_cc_role"] == "admin" || $_SESSION["mailcow_cc_role"] == "domainadmin") && $_SESSION["mailcow_cc_username"] == $object) { - $data = $u2f->getRegisterData(get_u2f_registrations($object)); - list($req, $sigs) = $data; + list($req, $sigs) = $u2f->getRegisterData(get_u2f_registrations($object)); $_SESSION['regReq'] = json_encode($req); - echo 'var req = ' . json_encode($req) . '; var sigs = ' . json_encode($sigs) . ';'; + $_SESSION['regSigs'] = json_encode($sigs); + echo 'var req = ' . json_encode($req) . ';'; + echo 'var registeredKeys = ' . json_encode($sigs) . ';'; + echo 'var appId = req.appId;'; + echo 'var registerRequests = [{version: req.version, challenge: req.challenge}];'; } else { return; @@ -1354,9 +1357,19 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u case "u2f-authentication": header('Content-Type: application/javascript'); if (isset($_SESSION['pending_mailcow_cc_username']) && $_SESSION['pending_mailcow_cc_username'] == $object) { - $reqs = json_encode($u2f->getAuthenticateData(get_u2f_registrations($object))); - $_SESSION['authReq'] = $reqs; - echo 'var req = ' . $reqs . ';'; + $auth_data = $u2f->getAuthenticateData(get_u2f_registrations($object)); + $challenge = $auth_data[0]->challenge; + $appId = $auth_data[0]->appId; + foreach ($auth_data as $each) { + $key = array(); // Empty array + $key['version'] = $each->version; + $key['keyHandle'] = $each->keyHandle; + $registeredKey[] = $key; + } + $_SESSION['authReq'] = json_encode($auth_data); + echo 'var appId = "' . $appId . '";'; + echo 'var challenge = ' . json_encode($challenge) . ';'; + echo 'var registeredKeys = ' . json_encode($registeredKey) . ';'; } else { return;