Crypto::hashCrypt()   A
last analyzed

Complexity

Conditions 2
Paths 3

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 7
nc 3
nop 1
dl 0
loc 13
rs 10
c 1
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace AbterPhp\Framework\Crypto;
6
7
use AbterPhp\Framework\Exception\Security as SecurityException;
8
use Exception;
9
use Opulence\Cryptography\Encryption\IEncrypter;
10
use Opulence\Cryptography\Hashing\IHasher;
11
12
class Crypto
13
{
14
    protected const ERROR_INVALID_SECRET = 'packed password must be a valid SHA3-512 hash';
15
16
    protected const SECRET_HASH_LENGTH = 128;
17
18
    protected IHasher $hasher;
19
20
    protected IEncrypter $encrypter;
21
22
    protected string $pepper = '';
23
24
    /** @var array<string,string> */
25
    protected array $hashOptions = [];
26
27
    protected string $frontendSalt = '';
28
29
    protected string $rawSecretRegexp;
30
31
    /**
32
     * Authenticator constructor.
33
     *
34
     * @param IEncrypter           $encrypter
35
     * @param IHasher              $hasher
36
     * @param string               $pepper
37
     * @param array<string,string> $hashOptions
38
     * @param string               $frontendSalt
39
     */
40
    public function __construct(
41
        IEncrypter $encrypter,
42
        IHasher $hasher,
43
        string $pepper,
44
        array $hashOptions,
45
        string $frontendSalt
46
    ) {
47
        $this->hasher       = $hasher;
48
        $this->encrypter    = $encrypter;
49
        $this->pepper       = $pepper;
50
        $this->hashOptions  = $hashOptions;
51
        $this->frontendSalt = $frontendSalt;
52
53
        $this->rawSecretRegexp = sprintf('/^[0-9a-f]{%s}$/', static::SECRET_HASH_LENGTH);
54
    }
55
56
    /**
57
     * This method is used to "fake" frontend hashing. Use with care!
58
     *
59
     * @param string $rawText
60
     *
61
     * @return string
62
     */
63
    public function prepareSecret(string $rawText): string
64
    {
65
        return hash('sha3-512', $this->frontendSalt . $rawText);
66
    }
67
68
    /**
69
     * @param string $rawSecret SHA3-512 encoded secret in hexadecimal
70
     *
71
     * @return string secret hashed and encrypted
72
     * @throws SecurityException
73
     */
74
    public function hashCrypt(string $rawSecret): string
75
    {
76
        $this->assertRawSecret($rawSecret);
77
78
        try {
79
            $hashedSecret = $this->hasher->hash($rawSecret, $this->hashOptions, $this->pepper);
80
81
            $hashCryptedSecret = $this->encrypter->encrypt($hashedSecret);
82
        } catch (Exception $e) {
83
            throw new SecurityException($e->getMessage(), $e->getCode(), $e);
84
        }
85
86
        return $hashCryptedSecret;
87
    }
88
89
    /**
90
     * @param string $secret       SHA3-512 encoded secret in hexadecimal
91
     * @param string $storedSecret hashed and encrypted secret to compare $secret against
92
     *
93
     * @return bool
94
     * @throws SecurityException
95
     */
96
    public function verifySecret(string $secret, string $storedSecret): bool
97
    {
98
        try {
99
            $hashedSecret = $this->encrypter->decrypt($storedSecret);
100
101
            $verified = $this->hasher->verify($hashedSecret, $secret, $this->pepper);
102
        } catch (Exception $e) {
103
            throw new SecurityException($e->getMessage(), $e->getCode(), $e);
104
        }
105
106
        return $verified;
107
    }
108
109
    /**
110
     * @param string $secret
111
     */
112
    protected function assertRawSecret(string $secret): void
113
    {
114
        if (mb_strlen($secret) !== static::SECRET_HASH_LENGTH) {
115
            throw new SecurityException(static::ERROR_INVALID_SECRET);
116
        }
117
118
        if (!preg_match($this->rawSecretRegexp, $secret)) {
119
            throw new SecurityException(static::ERROR_INVALID_SECRET);
120
        }
121
    }
122
}
123