Response::getPublicKeyFromResponse()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 6
c 1
b 0
f 0
dl 0
loc 12
rs 10
cc 3
nc 3
nop 1
1
<?php declare(strict_types=1);
2
3
namespace ncryptf;
4
5
use Exception;
6
use InvalidArgumentException;
7
use SodiumException;
8
use ncryptf\exceptions\DecryptionFailedException;
9
use ncryptf\exceptions\InvalidChecksumException;
10
use ncryptf\exceptions\InvalidSignatureException;
11
12
class Response
13
{
14
    /**
15
     * Secret key
16
     *
17
     * @var string
18
     */
19
    private $secretKey;
20
21
    /**
22
     * Constructor
23
     *
24
     * @param string $secretKey The 32 byte secret key
25
     *
26
     * @throws InvalidArgumentException
27
     */
28
    public function __construct(string $secretKey)
29
    {
30
        if (\strlen($secretKey) !== SODIUM_CRYPTO_BOX_SECRETKEYBYTES) {
31
            throw new InvalidArgumentException(sprintf("Secret key should be %d bytes.", SODIUM_CRYPTO_BOX_SECRETKEYBYTES));
32
        }
33
34
        $this->secretKey = $secretKey;
35
    }
36
37
    /**
38
     * Decrypts a payload using the response and an optional nonce
39
     * Nonce is not required for v2 type signatures, but is required for v1 signatures
40
     *
41
     * @param string $response  The encrypted HTTP response, as a multi-byte string
42
     * @param string $publicKey 32 byte optional public key
43
     * @param string $nonce     The 32 byte nonce, optional
44
     *
45
     * @throws InvalidArgumentException
46
     */
47
    public function decrypt(string $response, string $publicKey = null, string $nonce = null) : string
48
    {
49
        $version = static::getVersion($response);
50
        if ($version === 2) {
51
            if (\strlen($response) < 236) {
52
                throw new InvalidArgumentException(sprintf("Message is %d bytes, however 236+ were expected", \strlen($response)));
53
            }
54
55
            $nonce = \substr($response, 4, 24);
56
57
            // Determine the payload size sans the 64 byte checksum at the end
58
            $payload = \substr($response, 0, \strlen($response) - 64);
59
            $checksum = \substr($response, -64);
60
61
            // Verify the checksum to ensure the headers haven't been tampered with
62
            if (\sodium_memcmp($checksum, \sodium_crypto_generichash($payload, $nonce, 64)) !== 0) {
63
                throw new InvalidChecksumException("Calculated checksum differs from the checksum associated with the message.");
64
            }
65
66
            $publicKey = self::getPublicKeyFromResponse($response);
67
            $signature = \substr($payload, -64);
68
            $payload = \substr($payload, 0, -64);
69
            $sigPubKey = self::getSigningPublicKeyFromResponse($response);
70
            $payload = \substr($payload, 0, -32);
71
            $body = \substr($payload, 60, \strlen($payload));
72
73
            $decryptedPayload = $this->decryptBody($body, $publicKey, $nonce);
74
75
            if (!$this->isSignatureValid($decryptedPayload, $signature, $sigPubKey)) {
76
                throw new InvalidSignatureException('The message signature is not valid.');
77
            }
78
79
            return $decryptedPayload;
80
        }
81
82
        if ($nonce === null) {
83
            throw new InvalidArgumentException('Nonce is required to decrypt v1 requests.');
84
        }
85
86
        if ($publicKey === null || \strlen($publicKey) !== SODIUM_CRYPTO_BOX_PUBLICKEYBYTES) {
87
            throw new InvalidArgumentException(sprintf("Public key should be %d bytes.", SODIUM_CRYPTO_BOX_PUBLICKEYBYTES));
88
        }
89
90
        return $this->decryptBody($response, $publicKey, $nonce);
91
    }
92
93
    /**
94
     * Decrypts a given response with a nonce
95
     * This will return the decrypted string of decrypt was successful, and false otherwise
96
     *
97
     * @param string $response  The encrypted HTTP response, as a multi-byte string
98
     * @param string $publicKey 32 byte public key
99
     * @param string $nonce     The 32 byte nonce
100
     * @return string
101
     *
102
     * @throws InvalidArgumentException
103
     */
104
    private function decryptBody(string $response, string $publicKey, string $nonce) : string
105
    {
106
        try {
107
            if (\strlen($publicKey) !== SODIUM_CRYPTO_BOX_PUBLICKEYBYTES) {
108
                throw new InvalidArgumentException(sprintf("Public key should be %d bytes.", SODIUM_CRYPTO_BOX_PUBLICKEYBYTES));
109
            }
110
111
            if (\strlen($nonce) !== SODIUM_CRYPTO_BOX_NONCEBYTES) {
112
                throw new InvalidArgumentException(sprintf("Nonce should be %d bytes.", SODIUM_CRYPTO_BOX_NONCEBYTES));
113
            }
114
115
            if (\strlen($response) < SODIUM_CRYPTO_BOX_MACBYTES) {
116
                throw new DecryptionFailedException("Minimum message length not met.");
117
            }
118
119
            $keypair = new Keypair(
120
                $this->secretKey,
121
                $publicKey
122
            );
123
124
            $result = \sodium_crypto_box_open(
125
                $response,
126
                $nonce,
127
                $keypair->getSodiumKeypair()
128
            );
129
130
            if ($result !== false) {
131
                return $result;
132
            }
133
134
            throw new DecryptionFailedException;
135
        } catch (SodiumException $e) {
136
            throw new InvalidArgumentException($e->getMessage());
137
        }
138
    }
139
140
    /**
141
     * Returns true if the signature validates the response
142
     *
143
     * @param string $response  The raw http response, after decoding
144
     * @param string $signature The raw multi-byte signature
145
     * @param string $publicKey The signing public key
146
     * @return bool
147
     *
148
     * @throws InvalidArgumentException
149
     */
150
    public function isSignatureValid(string $response, string $signature, string $publicKey) : bool
151
    {
152
        if (\strlen($publicKey) !== SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES) {
153
            throw new InvalidArgumentException(sprintf("Public key should be %d bytes.", SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES));
154
        }
155
156
        if (\strlen($signature) !== 64) {
157
            throw new InvalidArgumentException(sprintf("Signature %d bytes.", 64));
158
        }
159
160
        try {
161
            return \sodium_crypto_sign_verify_detached(
162
                $signature,
163
                $response,
164
                $publicKey
165
            );
166
        } catch (SodiumException $e) {
167
            throw new InvalidArgumentException($e->getMessage());
168
        }
169
    }
170
171
    /**
172
     * Extracts the public key from a v2 response
173
     *
174
     * @param string $response
175
     * @return string
176
     */
177
    public static function getPublicKeyFromResponse(string $response) : string
178
    {
179
        $version = static::getVersion($response);
180
        if ($version === 2) {
181
            if (\strlen($response) < 236) {
182
                throw new InvalidArgumentException;
183
            }
184
185
            return \substr($response, 28, 32);
186
        }
187
188
        throw new InvalidArgumentException('The response provided is not suitable for public key extraction.');
189
    }
190
191
    /**
192
     * Extracts the signing public key from a v2 response
193
     *
194
     * @param string $response
195
     * @return string
196
     */
197
    public static function getSigningPublicKeyFromResponse(string $response) : string
198
    {
199
        $version = static::getVersion($response);
200
        if ($version === 2) {
201
            if (\strlen($response) < 236) {
202
                throw new InvalidArgumentException;
203
            }
204
205
            return \substr($response, -160, 32);
206
        }
207
208
        throw new InvalidArgumentException('The response provided is not suitable for public key extraction.');
209
    }
210
211
    /**
212
     * Extracts the version from the response
213
     *
214
     * @param string $response  The encrypted http response
215
     * @return int
216
     */
217
    public static function getVersion(string $response) : int
218
    {
219
        if (\strlen($response) < 16) {
220
            throw new DecryptionFailedException("Message length is too short to determine version.");
221
        }
222
223
        $header = \substr($response, 0, 4);
224
        if (\strtoupper(\bin2hex($header)) === 'DE259002') {
225
            return 2;
226
        }
227
228
        return 1;
229
    }
230
}
231