Passed
Push — master ( 1f909f...68b106 )
by Thomas
07:19
created

JsonConverter::decodeAssertionString()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace MadWizard\WebAuthn\Json;
4
5
use MadWizard\WebAuthn\Dom\AuthenticatorAssertionResponse;
6
use MadWizard\WebAuthn\Dom\AuthenticatorAttestationResponse;
7
use MadWizard\WebAuthn\Dom\AuthenticatorResponseInterface;
8
use MadWizard\WebAuthn\Dom\DictionaryInterface;
9
use MadWizard\WebAuthn\Dom\PublicKeyCredential;
10
use MadWizard\WebAuthn\Dom\PublicKeyCredentialInterface;
11
use MadWizard\WebAuthn\Dom\PublicKeyCredentialType;
12
use MadWizard\WebAuthn\Exception\ParseException;
13
use MadWizard\WebAuthn\Exception\WebAuthnException;
14
use MadWizard\WebAuthn\Format\Base64UrlEncoding;
15
use MadWizard\WebAuthn\Format\ByteBuffer;
16
use MadWizard\WebAuthn\Format\DataValidator;
17
use function is_string;
18
19
final class JsonConverter
20
{
21
    /**
22
     * @codeCoverageIgnore
23
     */
24
    private function __construct()
25
    {
26
    }
27
28
    /**
29
     * Parses a JSON string containing a credential returned from the JS credential API's credentials.get or
30
     * credentials.create. The JSOn structure matches the PublicKeyCredential interface from the WebAuthn specifications
31
     * closely but since it contains ArrayBuffers it cannot be directly converted to a JSON equivalent. Fields that
32
     * are ArrayBuffers are assumed to be base64url encoded.
33
     *
34
     * Also, the response field of the PublicKeyCredential can contain either an attestation or assertion response.
35
     * To determine which one to parse the $responseType parameter must be set to 'attestation' or 'assertion'.
36
     *
37
     * The required JSON structure is:
38
     * ```
39
     * {
40
     *   "type": "public-key",
41
     *   "id": "base64url encoded ArrayBuffer",
42
     *   "response" : << authenticator response >>,
43
     *   "getClientExtensionResults" : << output of credential.getClientExtensionResults() >>
44
     * }
45
     * ```
46
     *
47
     * Where the authenticator response for attestation is:
48
     * ```
49
     * {
50
     *   attestationObject: "base64url encoded ArrayBuffer"
51
     * }
52
     * ```
53
     * and for assertion response is:
54
     * ```
55
     * {
56
     *   authenticatorData : "base64url encoded ArrayBuffer",
57
     *   signature: "base64url encoded ArrayBuffer",
58
     *   userHandle: "base64url encoded ArrayBuffer"
59
     * }
60
     * ```
61
     *
62
     * @param array  $json         Json data (with objects as associative arrays)
63
     * @param string $responseType Expected type of response in the public key's response field.
64
     *                             Either 'attestation' for attestation responses or 'assertion' for assertion responses.
65
     *
66
     * @throws ParseException
67
     *
68
     * @see https://www.w3.org/TR/webauthn/#publickeycredential
69
     */
70 27
    public static function decodeCredential(array $json, string $responseType): PublicKeyCredentialInterface
71
    {
72 27
        if (($json['type'] ?? null) !== PublicKeyCredentialType::PUBLIC_KEY) {
73 1
            throw new ParseException("Expecting type 'public-key'");
74
        }
75
76 26
        if (empty($json['id'])) {
77 1
            throw new ParseException('Missing id in json data');
78
        }
79 25
        $id = $json['id'];
80 25
        if (!is_string($id)) {
81 1
            throw new ParseException('Id in json data should be a string');
82
        }
83
84 24
        $rawId = Base64UrlEncoding::decode($id);
85
86 23
        $responseData = $json['response'] ?? null;
87 23
        if (!is_array($responseData)) {
88 1
            throw new ParseException('Expecting array data for response');
89
        }
90
91 22
        $response = self::decodeResponse($responseData, $responseType);
92
93
        // TODO: clientExtensionResults
94
95 19
        return new PublicKeyCredential(new ByteBuffer($rawId), $response);
96
    }
97
98 20
    private static function jsonFromString(string $json): array
99
    {
100 20
        $decoded = json_decode($json, true, 10);
101 20
        if (!is_array($decoded)) {
102 1
            throw new ParseException('Failed to decode PublicKeyCredential Json');
103
        }
104 19
        return $decoded;
105
    }
106
107
    public static function decodeAttestationString(string $json): PublicKeyCredentialInterface
108
    {
109
        return self::decodeCredential(self::jsonFromString($json), 'attestation');
110
    }
111
112 20
    public static function decodeAssertionString(string $json): PublicKeyCredentialInterface
113
    {
114 20
        return self::decodeCredential(self::jsonFromString($json), 'assertion');
115
    }
116
117 6
    public static function decodeAttestation(array $json): PublicKeyCredentialInterface
118
    {
119 6
        return self::decodeCredential($json, 'attestation');
120
    }
121
122
    public static function decodeAssertion(array $json): PublicKeyCredentialInterface
123
    {
124
        return self::decodeCredential($json, 'assertion');
125
    }
126
127 22
    private static function decodeResponse(array $response, string $responseType): AuthenticatorResponseInterface
128
    {
129 22
        $clientDataJson = $response['clientDataJSON'] ?? null;
130
131 22
        if (!is_string($clientDataJson)) {
132 1
            throw new ParseException('Expecting client data json');
133
        }
134 21
        $clientDataJson = Base64UrlEncoding::decode($clientDataJson);
135
136 21
        if ($responseType === 'assertion') {
137 15
            return self::decodeAssertionResponse($clientDataJson, $response);
138
        }
139 6
        if ($responseType === 'attestation') {
140 5
            return self::decodeAttestationResponse($clientDataJson, $response);
141
        }
142 1
        throw new WebAuthnException(sprintf('Unknown or missing type %s', $responseType));
143
    }
144
145 15
    private static function decodeAssertionResponse(string $clientDataJson, array $response): AuthenticatorAssertionResponse
146
    {
147 15
        DataValidator::checkTypes(
148 15
            $response,
149
            [
150 15
                'authenticatorData' => 'string',
151
                'signature' => 'string',
152
                'userHandle' => '?:string',
153
            ],
154 15
            false
155
        );
156
157 15
        $authenticatorData = new ByteBuffer(Base64UrlEncoding::decode($response['authenticatorData']));
158 15
        $signature = new ByteBuffer(Base64UrlEncoding::decode($response['signature']));
159
160 15
        $userHandle = null;
161
162 15
        $encUserHandle = $response['userHandle'] ?? null;
163 15
        if ($encUserHandle !== null) {
164 2
            $userHandle = new ByteBuffer(Base64UrlEncoding::decode($encUserHandle));
165
        }
166
167 15
        return new AuthenticatorAssertionResponse($clientDataJson, $authenticatorData, $signature, $userHandle);
168
    }
169
170 5
    private static function decodeAttestationResponse(string $clientDataJson, array $response): AuthenticatorAttestationResponse
171
    {
172 5
        DataValidator::checkTypes(
173 5
            $response,
174
            [
175 5
                'attestationObject' => 'string',
176
            ],
177 5
            false
178
        );
179
180 5
        return new AuthenticatorAttestationResponse(
181 5
            $clientDataJson,
182 5
            new ByteBuffer(Base64UrlEncoding::decode($response['attestationObject']))
183
        );
184
    }
185
186 8
    public static function encodeDictionary(DictionaryInterface $dictionary): array
187
    {
188 8
        return self::encodeArray($dictionary->getAsArray());
189
    }
190
191 8
    private static function encodeArray(array $map): array
192
    {
193 8
        $converted = [];
194 8
        foreach ($map as $key => $value) {
195 8
            if ($value instanceof ByteBuffer) {
196
                // There is no direct way to store a ByteBuffer in JSON string easily.
197
                // Encode as base46url encoded string
198 7
                $converted[$key] = Base64UrlEncoding::encode($value->getBinaryString());
199 7
            } elseif ($value instanceof DictionaryInterface) {
200 5
                $converted[$key] = self::encodeDictionary($value);
201 7
            } elseif (\is_scalar($value)) {
202 7
                $converted[$key] = $value;
203 6
            } elseif (\is_array($value)) {
204 6
                $converted[$key] = self::encodeArray($value);
205
            } else {
206
                throw new WebAuthnException('Cannot convert this data to JSON format');
207
            }
208
        }
209 8
        return $converted;
210
    }
211
}
212