Completed
Push — master ( d865ae...34c801 )
by Charles
03:27
created

Response::decryptBody()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 7
dl 0
loc 10
c 0
b 0
f 0
rs 10
cc 2
nc 2
nop 2
1
<?php declare(strict_types=1);
2
3
namespace ncryptf;
4
5
use ncryptf\exceptions\InvalidChecksumException;
6
use ncryptf\exceptions\InvalidSignatureException;
7
use InvalidArgumentException;
8
use SodiumException;
9
10
class Response
11
{
12
    /**
13
     * Sodium CryptoBox Keypair
14
     *
15
     * @var string
16
     */
17
    private $keypair;
18
19
    /**
20
     * Constructor
21
     *
22
     * @param string $secretKey The 32 byte secret key
23
     * @param string $publicKey The 32 byte public key
24
     * 
25
     * @throws InvalidArguementException
26
     */
27
    public function __construct(string $secretKey, string $publicKey)
28
    {
29
        try {
30
            $this->keypair = \sodium_crypto_box_keypair_from_secretkey_and_publickey(
31
                $secretKey,
32
                $publicKey
33
            );
34
        } catch (SodiumException $e) {
35
            throw new InvalidArgumentException($e->getMessage());
36
        }
37
    }
38
39
    /**
40
     * Decrypts a payload using the response and an optional nonce
41
     * Nonce is not required for v2 type signatures, but is required for v1 signatures
42
     * 
43
     * @param string $response  The encrypted HTTP response, as a multi-byte string
44
     * @param string $nonce     The 32 byte nonce
45
     * 
46
     * @throws InvalidArguementException
47
     */
48
    public function decrypt(string $response, string $nonce = null) : string
49
    {
50
        $version = $this->getVersion($response);
51
        if ($version === 2) {
52
            $nonce = \substr($response, 4, 24);
53
            // Determine the payload size sans the 64 byte checksum at the end
54
            $payload = \substr($response, 0, \strlen($response) - 64);
55
            $checksum = \substr($response, -64);
56
57
            // Verify the checksum to ensure the headers haven't been tampered with
58
            if ($checksum !== \sodium_crypto_generichash($payload, $nonce, 64)) {
59
                throw new InvalidChecksumException;
60
            }
61
            
62
            $publicKey = \substr($response, 28, 32);
0 ignored issues
show
Unused Code introduced by
The assignment to $publicKey is dead and can be removed.
Loading history...
63
            $signature = \substr($payload, -64);
64
            $payload = \substr($payload, 0, -64);
65
            $sigPubKey = \substr($payload, -32);
66
            $payload = \substr($payload, 0, -32);
67
            $body = \substr($payload, 60, \strlen($payload));
68
69
            $decryptedPayload = $this->decrypt($body, $nonce);
70
            if (!$this->isSignatureValid($decryptedPayload, $signature, $sigPubKey)) {
71
                throw new InvalidSignatureException;
72
            }
73
74
            return $decryptedPayload;
75
        }
76
77
        if ($nonce === null) {
78
            throw new InvalidArgumentException('Nonce is required to decrypt v1 requests.');
79
        }
80
81
        return $this->decryptBody($response, $nonce);
82
    }
83
84
    /**
85
     * Decrypts a given response with a nonce
86
     * This will return the decrypted string of decrypt was successful, and false otherwise
87
     *
88
     * @param string $response  The encrypted HTTP response, as a multi-byte string
89
     * @param string $nonce     The 32 byte nonce
90
     * @return string
91
     * 
92
     * @throws InvalidArguementException
93
     */
94
    private function decryptBody(string $response, string $nonce) : string
95
    {
96
        try {
97
            return \sodium_crypto_box_open(
98
                $response,
99
                $nonce,
100
                $this->keypair
101
            );
102
        } catch (SodiumException $e) {
103
            throw new InvalidArgumentException($e->getMessage());
104
        }
105
    }
106
107
    /**
108
     * Returns true if the signature validates the response
109
     *
110
     * @param string $response  The raw http response, after decoding
111
     * @param string $signature The raw multi-byte signature
112
     * @param string $publicKey The signing public key
113
     * @return bool
114
     * 
115
     *  @throws InvalidArguementException
116
     */
117
    public function isSignatureValid(string $response, string $signature, string $publicKey) : bool
118
    {
119
        try {
120
            return \sodium_crypto_sign_verify_detached(
121
                $signature,
122
                $response,
123
                $publicKey
124
            );
125
        } catch (SodiumException $e) {
126
            throw new InvalidArgumentException($e->getMessage());
127
        }
128
    }
129
130
    /**
131
     * Extracts the version from the response
132
     * 
133
     * @param string $response  The encrypted http response
134
     * @return int
135
     */
136
    private function getVersion(string $response) : int
137
    {
138
        $header = \substr($response, 0, 4);
139
        if (\strtoupper(\unpack("h*", $header)[1]) === 'DE259002') {
140
            return 2;
141
        }
142
143
        return 1;
144
    }
145
}
146