Skip to content

Instantly share code, notes, and snippets.

@mvrhov
Created May 17, 2018 07:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mvrhov/e297980a058f13f38e7828bd925b7fe6 to your computer and use it in GitHub Desktop.
Save mvrhov/e297980a058f13f38e7828bd925b7fe6 to your computer and use it in GitHub Desktop.
Split token
<?php declare(strict_types=1);
/**
* Released under the MIT License.
*
* Copyright (c) 2018 Miha Vrhovnik <miha.vrhovnik@gmail.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
namespace Component\Security\Token;
/**
* Based on approach written on
* https://paragonie.com/blog/2016/09/untangling-forget-me-knot-secure-account-recovery-made-simple
*/
class SplitToken
{
public const SELECTOR_LENGTH = 21;
public const VERIFIER_LENGTH = 18;
private const SELECTOR_LENGTH_ENCODED = self::SELECTOR_LENGTH * 4 / 3;
private const VERIFIER_LENGTH_ENCODED = self::VERIFIER_LENGTH * 4 / 3;
public const TOKEN_LENGTH = 1 + self::SELECTOR_LENGTH_ENCODED + self::VERIFIER_LENGTH_ENCODED;
private const TOKEN_VERSION = '1'; //we have more than enough versions: From 1-9a-zA-Z
/** @var string */
private $verifier;
/** @var string */
private $verifierHash;
/** @var string */
private $selector;
/** @var string */
private $token;
/**
* @param null|string $salt
*
* @return self
*/
public static function generate(?string $salt = null): self
{
$base64 = function ($value) {
return rtrim(strtr(base64_encode($value), '+/', '-_'), '=');
};
$return = new self();
$return->verifier = $base64(random_bytes(self::VERIFIER_LENGTH));
$return->selector = $base64(random_bytes(self::SELECTOR_LENGTH));
$return->token = self::TOKEN_VERSION . $return->verifier . $return->selector;
$salt = $salt ?? "\0";
$return->verifierHash = password_hash('salt:' . $salt . $return->verifier, PASSWORD_BCRYPT);
return $return;
}
/**
* @param string $token
*
* @return self
*/
public static function fromToken(string $token): self
{
$return = new self();
$version = substr($token, 0, 1);
if ($version !== self::TOKEN_VERSION) {
throw new \RuntimeException('Got unsupported token version.');
}
if (\strlen($token) !== self::TOKEN_LENGTH) {
throw new \RuntimeException('Token is of unsupported length.');
}
$return->token = $token;
$return->verifier = substr($token, 1, self::VERIFIER_LENGTH_ENCODED);
$return->selector = substr($token, 1 + self::VERIFIER_LENGTH_ENCODED, self::SELECTOR_LENGTH_ENCODED);
return $return;
}
/**
* Returns selector part of the token, that's supposed to be stored into the database
* It's the unique across all users
*/
public function getSelector(): string
{
return $this->selector;
}
/**
* Returns full authentication token, it MUST NOT not be stored anywhere,
* this is the string that the user should receive
*/
public function getToken(): string
{
return $this->token;
}
/**
* @param string $selector as stored in the database
* @param string $verifierHash as stored in the database
* @param null|string $salt the same salt that was used for generating original split token
*
* @return bool
*/
public function matches(string $selector, string $verifierHash, ?string $salt = null): bool
{
//both selector and the hash should match
if ($this->selector !== $selector) {
return false;
}
$salt = $salt ?? "\0";
return password_verify('salt:' . $salt . $this->verifier, $verifierHash);
}
/**
* Returns verifier that's supposed to be stored into the database
*/
public function getVerifierHash(): string
{
if (null === $this->verifierHash) {
throw new \RuntimeException('verifier hash is not calculated when object is build from token');
}
return $this->verifierHash;
}
/**
* Returns full authentication token, it MUST NOT not be stored anywhere,
* this is the string that the user should receive
*/
public function __toString(): string
{
return $this->token;
}
public function toValueHolder(\DateTimeImmutable $expiresAt, array $metadata = []): SplitTokenValueHolder
{
return new SplitTokenValueHolder($this->selector, $this->verifierHash, $expiresAt, $metadata);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment