<?php
declare(strict_types=1);
namespace ParagonIE\Discretion\Struct;
use ParagonIE\ConstantTime\Base64UrlSafe;
use ParagonIE\Discretion\{
Discretion,
SimpleCrypto,
Struct
};
use ParagonIE\Discretion\Data\HiddenString;
use ParagonIE\Discretion\Exception\RecordNotFound;
use ParagonIE\Discretion\Policies\Unique;
/**
* Class User
* @package ParagonIE\Discretion\Struct
*/
class User extends Struct implements Unique
{
const BCRYPT_COST = 12;
const TABLE_NAME = 'discretion_users';
const PRIMARY_KEY = 'userid';
const DB_FIELD_NAMES = [
'userid' => 'id',
'active' => 'active',
'username' => 'username',
'pwhash' => 'pwHash',
'twofactor' => 'twoFactorSecret',
'email' => 'email',
'fullname' => 'fullName',
'chronicle' => 'chronicle'
];
const BOOLEAN_FIELDS = ['active'];
/** @var bool $active */
protected $active = false;
/** @var string $chronicle */
protected $chronicle = '';
/** @var string $email */
protected $email = '';
/** @var string $fullName */
protected $fullName = '';
/** @var string $username */
protected $username = '';
/** @var string $pwHash */
protected $pwHash = '';
/** @var string $twoFactorSecret */
protected $twoFactorSecret = '';
/**
* @param string $username
* @return self
* @throws RecordNotFound
*/
public static function byUsername(string $username): self
{
/** @var int $userId */
$userId = Discretion::getDatabase()->cell(
"SELECT userid FROM discretion_users WHERE username = ?",
$username
);
if (empty($userId)) {
throw new RecordNotFound('No user with the username ' . $username);
}
return static::byId((int) $userId);
}
/**
* @param HiddenString $password
* @return bool
*/
public function checkPassword(HiddenString $password): bool
{
$preHash = Base64UrlSafe::encode(
\ParagonIE_Sodium_Compat::crypto_generichash(
$password->getString(),
'',
54
)
);
$stored = SimpleCrypto::decrypt(
$this->pwHash,
Discretion::getLocalEncryptionKey()
);
return \password_verify(
$preHash,
$stored->getString()
);
}
/**
* @return HiddenString
*/
public function get2FASecret(): HiddenString
{
return SimpleCrypto::decrypt(
$this->twoFactorSecret,
Discretion::getLocalEncryptionKey()
);
}
/**
* @return bool
*/
public function isActive(): bool
{
return $this->active;
}
/**
* Overwrite the 2FA secret.
*
* @param HiddenString $secret
* @return self
*/
public function set2FASecret(HiddenString $secret): self
{
$this->twoFactorSecret = SimpleCrypto::encrypt(
$secret,
Discretion::getLocalEncryptionKey()
);
return $this;
}
/**
* @param bool $isActive
* @return self
*/
public function setActive(bool $isActive): self
{
$this->active = $isActive;
return $this;
}
/**
* @param string $email
* @return self
*/
public function setEmail(string $email): self
{
$this->email = $email;
return $this;
}
/**
* @param string $fullName
* @return self
*/
public function setFullName(string $fullName): self
{
$this->fullName = $fullName;
return $this;
}
/**
* @param HiddenString $password
* @return self
*/
public function setPassword(HiddenString $password): self
{
// Prehash it to prevent bcrypt truncation.
$preHash = Base64UrlSafe::encode(
\ParagonIE_Sodium_Compat::crypto_generichash(
$password->getString(),
'',
54
)
);
$this->pwHash = SimpleCrypto::encrypt(
new HiddenString(
\password_hash(
$preHash,
PASSWORD_DEFAULT,
['cost' => static::BCRYPT_COST]
)
),
Discretion::getLocalEncryptionKey()
);
return $this;
}
/**
* @param string $username
* @return self
*/
public function setUsername(string $username): self
{
$this->username = $username;
return $this;
}
/**
* @param string $username
* @return bool
*/
public static function usernameIsTaken(string $username): bool
{
/** @psalm-suppress InvalidArgument Trust me on this one, Psalm. */
return Discretion::getDatabase()->exists(
"SELECT count(*) FROM discretion_users WHERE username = ?",
$username
);
}
}
|