Passed
Push — master ( 34c6af...d5f109 )
by Charles
02:18
created

Response::getSignaturePublicKeyFromResponse()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 7
dl 0
loc 13
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
     * 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 ($checksum !== \sodium_crypto_generichash($payload, $nonce, 64)) {
63
                throw new InvalidChecksumException("Calculated checksum differs from the checksum associated with the message.");
64
            }
65
            
66
            $publicKey = \substr($response, 28, 32);
67
            $signature = \substr($payload, -64);
68
            $payload = \substr($payload, 0, -64);
69
            $sigPubKey = \substr($payload, -32);
70
            $payload = \substr($payload, 0, -32);
71
            $body = \substr($payload, 60, \strlen($payload));
72
73
            $decryptedPayload = $this->decryptBody($body, $publicKey, $nonce);
74
            if (!$decryptedPayload) {
75
                throw new DecryptionFailedException('An unexpected error occurred when decrypting the message.');
76
            }
77
78
            if (!$this->isSignatureValid($decryptedPayload, $signature, $sigPubKey)) {
79
                throw new InvalidSignatureException('The message signature is not valid.');
80
            }
81
82
            return $decryptedPayload;
83
        }
84
85
        if ($nonce === null) {
86
            throw new InvalidArgumentException('Nonce is required to decrypt v1 requests.');
87
        }
88
89
        if (\strlen($publicKey) !== SODIUM_CRYPTO_BOX_PUBLICKEYBYTES) {
90
            throw new InvalidArgumentException(sprintf("Public key should be %d bytes.", SODIUM_CRYPTO_BOX_PUBLICKEYBYTES));
91
        }
92
        
93
        return $this->decryptBody($response, $publicKey, $nonce);
0 ignored issues
show
Bug introduced by
It seems like $publicKey can also be of type null; however, parameter $publicKey of ncryptf\Response::decryptBody() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

93
        return $this->decryptBody($response, /** @scrutinizer ignore-type */ $publicKey, $nonce);
Loading history...
94
    }
95
96
    /**
97
     * Decrypts a given response with a nonce
98
     * This will return the decrypted string of decrypt was successful, and false otherwise
99
     *
100
     * @param string $response  The encrypted HTTP response, as a multi-byte string
101
     * @param string $publicKey 32 byte public key
102
     * @param string $nonce     The 32 byte nonce
103
     * @return string
104
     *
105
     * @throws InvalidArgumentException
106
     */
107
    private function decryptBody(string $response, string $publicKey, string $nonce) : string
108
    {
109
        try {
110
            if (\strlen($publicKey) !== SODIUM_CRYPTO_BOX_PUBLICKEYBYTES) {
111
                throw new InvalidArgumentException(sprintf("Public key should be %d bytes.", SODIUM_CRYPTO_BOX_PUBLICKEYBYTES));
112
            }
113
114
            if (\strlen($response) < SODIUM_CRYPTO_BOX_MACBYTES) {
115
                throw new DecryptionFailedException("Minimum message length not met.");
116
            }
117
118
            $keypair = new Keypair(
119
                $this->secretKey,
120
                $publicKey
121
            );
122
123
            if ($result = \sodium_crypto_box_open(
124
                $response,
125
                $nonce,
126
                $keypair->getSodiumKeypair()
127
            )) {
128
                return $result;
129
            }
130
            
131
            throw new DecryptionFailedException;
132
        } catch (SodiumException $e) {
133
            throw new InvalidArgumentException($e->getMessage());
134
        }
135
    }
136
137
    /**
138
     * Returns true if the signature validates the response
139
     *
140
     * @param string $response  The raw http response, after decoding
141
     * @param string $signature The raw multi-byte signature
142
     * @param string $publicKey The signing public key
143
     * @return bool
144
     *
145
     * @throws InvalidArgumentException
146
     */
147
    public function isSignatureValid(string $response, string $signature, string $publicKey) : bool
148
    {
149
        try {
150
            return \sodium_crypto_sign_verify_detached(
151
                $signature,
152
                $response,
153
                $publicKey
154
            );
155
        } catch (SodiumException $e) {
156
            throw new InvalidArgumentException($e->getMessage());
157
        }
158
    }
159
160
    /**
161
     * Extracts the public key from a v2 response
162
     *
163
     * @param string $response
164
     * @return string
165
     */
166
    public static function getPublicKeyFromResponse(string $response) : string
167
    {
168
        $version = static::getVersion($response);
169
        if ($version === 2) {
170
            if (\strlen($response) < 236) {
171
                throw new InvalidArgumentException;
172
            }
173
            
174
            return \substr($response, 28, 32);
175
        }
176
177
        throw new InvalidArgumentException('The response provided is not suitable for public key extraction.');
178
    }
179
180
    /**
181
     * Extracts the version from the response
182
     *
183
     * @param string $response  The encrypted http response
184
     * @return int
185
     */
186
    public static function getVersion(string $response) : int
187
    {
188
        if (\strlen($response) < 16) {
189
            throw new DecryptionFailedException("Message length is too short to determine version.");
190
        }
191
192
        $header = \substr($response, 0, 4);
193
        if (\strtoupper(\bin2hex($header)) === 'DE259002') {
194
            return 2;
195
        }
196
197
        return 1;
198
    }
199
}
200