Completed
Push — master ( eaa085...85b293 )
by Jonathan
02:25
created

Token::getIv()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
rs 10
1
<?php
2
namespace Iron;
3
4
use DateTime;
5
6
final class Token
7
{
8
    const MAC_FORMAT_VERSION = '2';
9
    const MAC_PREFIX = 'Fe26.';
10
    const DIGEST_METHOD = 'sha256';
11
12
    /** @var PasswordInterface */
13
    private $password;
14
    /** @var string */
15
    private $salt;
16
    /** @var string */
17
    private $iv;
18
    /** @var string */
19
    private $cipherText;
20
    /** @var int */
21
    private $expiration;
22
    /** @var callable */
23
    private $saltGenerator;
24
    /** @var callable */
25
    private $keyProvider;
26
27
    /**
28
     * @param PasswordInterface $password
29
     * @param string $salt
30
     * @param string $iv
31
     * @param string $cipherText
32
     * @param int $expiration
33
     * @param callable|null $keyProvider
34
     * @param callable|null $saltGenerator
35
     */
36
    public function __construct(
37
        PasswordInterface $password,
38
        string $salt,
39
        string $iv,
40
        string $cipherText,
41
        $expiration = 0,
42
        callable $keyProvider = null,
43
        callable $saltGenerator = null
44
    ) {
45
        $this->password = $password;
46
        $this->salt = $salt;
47
        $this->iv = $iv;
48
        $this->cipherText = $cipherText;
49
        $this->expiration = $expiration;
50
        $this->keyProvider = $keyProvider ?: default_key_provider();
51
        $this->saltGenerator = $saltGenerator ?: default_salt_generator();
52
    }
53
54
    /**
55
     * @param PasswordInterface $password
56
     * @param string $sealed
57
     * @param bool $validate
58
     * @param callable|null $keyProvider
59
     * @param callable|null $saltGenerator
60
     *
61
     * @return Token
62
     */
63
    public static function fromSealed(
64
        PasswordInterface $password,
65
        string $sealed,
66
        bool $validate = true,
67
        callable $keyProvider = null,
68
        callable $saltGenerator = null
69
    ): Token {
70
        $parts = explode('*', $sealed);
71
        if (count($parts) !== 8) {
72
            throw new InvalidTokenException('Invalid token structure.');
73
        }
74
75
        list($version, $pwId, $salt, $iv, $cipherText, $ttd, $macSalt, $mac)
76
            = $parts;
77
78
        if ($version !== Token::MAC_PREFIX . Token::MAC_FORMAT_VERSION) {
79
            throw new InvalidTokenException('Invalid token version.');
80
        }
81
82
        if ($pwId !== $password->getId()) {
83
            throw new PasswordMismatchException(
84
                $password->getId(), 
85
                $pwId, 
86
                "Token encrypted with password $pwId; password"
87
                    . " {$password->getId()} provided for validation."
88
            );
89
        }
90
91
        $token = new self(
92
            $password,
93
            $salt,
94
            base64_decode($iv),
95
            base64_decode($cipherText),
96
            $ttd,
97
            $keyProvider,
98
            $saltGenerator
99
        );
100
101
        if ($validate) {
102
            if ($token->isExpired()) {
103
                throw new ExpiredTokenException('Token expired on '
104
                    . DateTime::createFromFormat('U', $ttd)->format('c'));
105
            }
106
107
            $token->validateChecksum($macSalt, $mac);
108
        }
109
110
        return $token;
111
    }
112
113
    public function __toString(): string
114
    {
115
        $stringToSign = $this->createStringToSign();
116
        $salt = generate_salt();
117
118
        return implode('*', [
119
            $stringToSign,
120
            $salt,
121
            $this->authenticateToken(
122
                $stringToSign,
123
                generate_key($this->password, $salt)
124
            ),
125
        ]);
126
    }
127
128
    public function validateChecksum(string $salt, string $expectedChecksum)
129
    {
130
        $actualChecksum = $this->authenticateToken(
131
            $this->createStringToSign(),
132
            generate_key($this->password, $salt)
133
        );
134
135
        if (!hash_equals($expectedChecksum, $actualChecksum)) {
136
            throw new InvalidTokenException('Invalid checksum.');
137
        }
138
    }
139
140
    public function getPasswordId(): string 
141
    {
142
        return $this->password->getId();
143
    }
144
145
    public function getSalt(): string
146
    {
147
        return $this->salt;
148
    }
149
150
    public function getIv(): string
151
    {
152
        return $this->iv;
153
    }
154
155
    public function getCipherText(): string 
156
    {
157
        return $this->cipherText;
158
    }
159
160
    /**
161
     * @return int|string
162
     */
163
    public function getExpiration()
164
    {
165
        return $this->expiration ?: '';
166
    }
167
168
    public function isExpired(int $gracePeriod = 0): bool 
169
    {
170
        return is_numeric($this->getExpiration())
171
            && $this->getExpiration() < time() + $gracePeriod;
172
    }
173
174
    private function authenticateToken(string $token, string $key): string 
175
    {
176
        return base64_encode(hash_hmac(self::DIGEST_METHOD, $token, $key, true));
177
    }
178
179
    private function createStringToSign(): string
180
    {
181
        return implode('*', [
182
            self::MAC_PREFIX . self::MAC_FORMAT_VERSION,
183
            $this->getPasswordId(),
184
            $this->getSalt(),
185
            base64_encode($this->getIv()),
186
            base64_encode($this->getCipherText()),
187
            $this->getExpiration(),
188
        ]);
189
    }
190
}
191