Completed
Push — master ( 1082d5...d6e326 )
by Charles
02:57
created

Response::getPublicKeyFromResponse()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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