Passed
Push — master ( f3ad4b...c4e72e )
by Robbie
05:06
created

CredentialRepository::setCredentials()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 6
rs 10
1
<?php declare(strict_types=1);
2
3
namespace SilverStripe\WebAuthn;
4
5
use InvalidArgumentException;
6
use Serializable;
7
use Webauthn\AttestedCredentialData;
8
use Webauthn\PublicKeyCredentialSource;
9
use Webauthn\PublicKeyCredentialSourceRepository;
10
use Webauthn\PublicKeyCredentialUserEntity;
11
12
/**
13
 * Implements the required interface from the WebAuthn library - but it does not implement the repository pattern in the
14
 * usual way. This is expected to be stored on a DataObject for persistence. Use the
15
 * @see CredentialRepository::hasChanged() API for determining whether a DataObject this is stored upon should be
16
 * persisted.
17
 */
18
class CredentialRepository implements PublicKeyCredentialSourceRepository, Serializable
19
{
20
    /**
21
     * @var string
22
     */
23
    private $memberID;
24
25
    /**
26
     * @var array
27
     */
28
    private $credentials = [];
29
30
    /**
31
     * @var bool
32
     */
33
    private $hasChanged = false;
34
35
    /**
36
     * @param string $memberID
37
     */
38
    public function __construct(string $memberID)
39
    {
40
        $this->memberID = $memberID;
41
    }
42
43
    public function has(string $credentialId): bool
44
    {
45
        return $this->findOneByCredentialId($credentialId) !== null;
46
    }
47
48
    public function get(string $credentialId): AttestedCredentialData
49
    {
50
        $this->assertCredentialID($credentialId);
51
52
        return $this->findOneByCredentialId($credentialId)->getAttestedCredentialData();
53
    }
54
55
    public function getUserHandleFor(string $credentialId): string
56
    {
57
        $this->assertCredentialID($credentialId);
58
59
        return $this->memberID;
60
    }
61
62
    public function getCounterFor(string $credentialId): int
63
    {
64
        $this->assertCredentialID($credentialId);
65
66
        return (int) $this->credentials[$this->getCredentialIDRef($credentialId)]['counter'];
67
    }
68
69
    public function updateCounterFor(string $credentialId, int $newCounter): void
70
    {
71
        $this->assertCredentialID($credentialId);
72
73
        $this->credentials[$this->getCredentialIDRef($credentialId)]['counter'] = $newCounter;
74
        $this->hasChanged = true;
75
    }
76
77
    /**
78
     * Assert that the given credential ID matches a stored credential
79
     *
80
     * @param string $credentialId
81
     */
82
    protected function assertCredentialID(string $credentialId): void
83
    {
84
        if (!$this->has($credentialId)) {
85
            throw new InvalidArgumentException('Given credential ID does not match any stored credentials');
86
        }
87
    }
88
89
    public function findOneByCredentialId(string $publicKeyCredentialId): ?PublicKeyCredentialSource
90
    {
91
        $ref = $this->getCredentialIDRef($publicKeyCredentialId);
92
        if (!isset($this->credentials[$ref])) {
93
            return null;
94
        }
95
96
        return $this->credentials[$ref]['source'];
97
    }
98
99
    /**
100
     * @return PublicKeyCredentialSource[]
101
     */
102
    public function findAllForUserEntity(PublicKeyCredentialUserEntity $publicKeyCredentialUserEntity): array
103
    {
104
        // Only return credentials if the user entity shares the same ID.
105
        if ($publicKeyCredentialUserEntity->getId() !== $this->memberID) {
106
            return [];
107
        }
108
109
        return array_map(function ($credentialComposite) {
110
            return $credentialComposite['source'];
111
        }, $this->credentials);
112
    }
113
114
    public function saveCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource): void
115
    {
116
        $ref = $this->getCredentialIDRef($publicKeyCredentialSource->getPublicKeyCredentialId());
117
118
        if (!isset($this->credentials[$ref])) {
119
            $this->credentials[$ref] = [
120
                'counter' => 0,
121
            ];
122
        }
123
124
        $this->credentials[$ref]['source'] = $publicKeyCredentialSource;
125
        $this->hasChanged = true;
126
    }
127
128
    /**
129
     * Empty the store deleting all stored credentials
130
     */
131
    public function reset(): void
132
    {
133
        $this->credentials = [];
134
    }
135
136
    /**
137
     * Indicates the repository has changed and should be persisted (as this doesn't follow the actual repository
138
     * pattern and is expected to be stored on a dataobject for persistence)
139
     *
140
     * @return bool
141
     */
142
    public function hasChanged(): bool
143
    {
144
        return $this->hasChanged;
145
    }
146
147
    /**
148
     * Set the credentials in bulk (for internal use) ensuring that credential objects are initialised correctly
149
     *
150
     * @param array $credentials
151
     */
152
    protected function setCredentials(array $credentials): void
153
    {
154
        $this->credentials = array_map(function ($data) {
155
            $data['source'] = PublicKeyCredentialSource::createFromArray($data['source']);
156
            return $data;
157
        }, $credentials);
158
    }
159
160
    /**
161
     * Create a reference to be used as a key for the credentials in the array
162
     *
163
     * @param string $credentialID
164
     * @return string
165
     */
166
    protected function getCredentialIDRef(string $credentialID): string
167
    {
168
        return base64_encode($credentialID);
169
    }
170
171
    /**
172
     * Provide the credentials stored in this repository as an array
173
     *
174
     * @return array
175
     */
176
    public function toArray(): array
177
    {
178
        return $this->credentials;
179
    }
180
181
    /**
182
     * Create an instance of a repository from the given credentials
183
     *
184
     * @param array $credentials
185
     * @param string $memberID
186
     * @return CredentialRepository
187
     */
188
    public static function fromArray(array $credentials, string $memberID): self
189
    {
190
        $new = new static($memberID);
191
        $new->setCredentials($credentials);
192
193
        return $new;
194
    }
195
196
    public function serialize()
197
    {
198
        return json_encode(['credentials' => $this->toArray(), 'memberID' => $this->memberID]);
199
    }
200
201
    public function unserialize($serialized)
202
    {
203
        $raw = json_decode($serialized, true);
204
205
        $this->memberID = $raw['memberID'];
206
        $this->setCredentials($raw['credentials']);
207
    }
208
}
209