Completed
Push — master ( 56c8c1...6c23d7 )
by Florent
01:45
created

PublicKeyCredentialLoader::retrieveExtensions()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 8
nc 2
nop 1
dl 0
loc 12
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * The MIT License (MIT)
7
 *
8
 * Copyright (c) 2014-2018 Spomky-Labs
9
 *
10
 * This software may be modified and distributed under the terms
11
 * of the MIT license.  See the LICENSE file for details.
12
 */
13
14
namespace U2FAuthentication\Fido2;
15
16
use Assert\Assertion;
17
use Base64Url\Base64Url;
18
use CBOR\CBORObject;
19
use CBOR\Decoder;
20
use CBOR\MapObject;
21
use CBOR\StringStream;
22
use U2FAuthentication\Fido2\AttestationStatement\AttestationObjectLoader;
23
use U2FAuthentication\Fido2\AuthenticationExtensions\AuthenticationExtension;
24
use U2FAuthentication\Fido2\AuthenticationExtensions\AuthenticationExtensionsClientOutputs;
25
26
class PublicKeyCredentialLoader
27
{
28
    private const FLAG_AT = 0b01000000;
29
    private const FLAG_ED = 0b10000000;
30
31
    private $attestationObjectLoader;
32
    private $decoder;
33
34
    public function __construct(AttestationObjectLoader $attestationObjectLoader, Decoder $decoder)
35
    {
36
        $this->attestationObjectLoader = $attestationObjectLoader;
37
        $this->decoder = $decoder;
38
    }
39
40
    public function load(string $data): PublicKeyCredential
41
    {
42
        $json = \Safe\json_decode($data, true);
43
        Assertion::keyExists($json, 'id');
44
        Assertion::keyExists($json, 'rawId');
45
        Assertion::keyExists($json, 'response');
46
47
        $id = Base64Url::decode($json['id']);
48
        $rawId = Base64Url::decode($json['rawId']);
49
        Assertion::true(hash_equals($id, $rawId));
50
51
        $publicKeyCredential = new PublicKeyCredential(
52
            $json['id'],
53
            $json['type'] ?? 'public-key',
54
            $rawId,
55
            $this->createResponse($json['response'])
56
        );
57
58
        return $publicKeyCredential;
59
    }
60
61
    private function createResponse(array $response): AuthenticatorResponse
62
    {
63
        Assertion::keyExists($response, 'clientDataJSON');
64
        switch (true) {
65
            case array_key_exists('attestationObject', $response):
66
                $attestationObject = $this->attestationObjectLoader->load($response['attestationObject']);
67
68
                return new AuthenticatorAttestationResponse(CollectedClientData::createFormJson($response['clientDataJSON']), $attestationObject);
69
            case array_key_exists('authenticatorData', $response) && array_key_exists('signature', $response):
70
                $authData = Base64Url::decode($response['authenticatorData']);
71
72
                $authDataStream = new StringStream($authData);
73
                $rp_id_hash = $authDataStream->read(32);
74
                $flags = $authDataStream->read(1);
75
                $signCount = $authDataStream->read(4);
76
                $signCount = unpack('N', $signCount)[1];
77
78
                $attestedCredentialData = null;
79
                if (\ord($flags) & self::FLAG_AT) {
80
                    $aaguid = $authDataStream->read(16);
81
                    $credentialLength = $authDataStream->read(2);
82
                    $credentialLength = unpack('n', $credentialLength)[1];
83
                    $credentialId = $authDataStream->read($credentialLength);
84
                    $credentialPublicKey = $this->decoder->decode($authDataStream);
85
                    Assertion::isInstanceOf($credentialPublicKey, MapObject::class, 'The data does not contain a valid credential public key.');
86
                    $attestedCredentialData = new AttestedCredentialData($aaguid, $credentialId, (string) $credentialPublicKey);
87
                }
88
89
                $extension = null;
90
                if (\ord($flags) & self::FLAG_ED) {
91
                    $extension = $this->decoder->decode($authDataStream);
92
                    $extension = $this->retrieveExtensions($extension);
93
                }
94
                $authenticatorData = new AuthenticatorData($authData, $rp_id_hash, $flags, $signCount, $attestedCredentialData, $extension);
95
96
                return new AuthenticatorAssertionResponse(
97
                    CollectedClientData::createFormJson($response['clientDataJSON']),
98
                    $authenticatorData,
99
                    Base64Url::decode($response['signature']),
100
                    $response['userHandle'] ?? null
101
                );
102
            default:
103
                throw new \InvalidArgumentException();
104
        }
105
    }
106
107
    private function retrieveExtensions(CBORObject $object): AuthenticationExtensionsClientOutputs
108
    {
109
        Assertion::isInstanceOf(MapObject::class, $object, 'Invalid extension object');
110
        $data = $object->getNormalizedData();
111
        Assertion::isArray($data, 'Invalid extension object');
112
        $extensions = new AuthenticationExtensionsClientOutputs();
113
        foreach ($data as $key => $value) {
114
            Assertion::string($key, 'Invalid extension key');
115
            $extensions->add(new AuthenticationExtension($key, $value));
116
        }
117
118
        return $extensions;
119
    }
120
}
121