Completed
Push — master ( c8b416...5bed71 )
by Charles
02:39
created

Json25519Parser::getRawBodyFromTokenAndNonce()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 17
rs 9.4285
cc 1
eloc 9
nc 1
nop 3
1
<?php
2
3
namespace yrc\web;
4
5
use yii\helpers\Json;
6
use yii\web\JsonParser;
7
use yrc\api\models\TokenKeyPair;
8
9
use yii\web\BadRequestHttpException;
10
use yii\base\InvalidParamException;
11
use Yii;
12
13
/**
14
 * Allows for requests to be encrypted and signed via Curve/Ed 25519 cryptography via libsodium
15
 * @class Json25519 Parser
16
 */
17
class Json25519Parser extends JsonParser
18
{
19
    /**
20
     * @const HASH_HEADER
21
     */
22
    const HASH_HEADER = 'x-hashid';
23
24
    /**
25
     * @const PUBLICKEY_HEADER
26
     */
27
    const PUBLICKEY_HEADER = 'x-pubkey';
28
29
    /**
30
     * @const NONCE_HEADER
31
     */
32
    const NONCE_HEADER = 'x-nonce';
33
34
    /**
35
     * Parses a HTTP request body.
36
     * @param string $rawBody the raw HTTP request body.
37
     * @param string $contentType the content type specified for the request body.
38
     * @return array parameters parsed from the request body
39
     * @throws BadRequestHttpException if the body contains invalid json and [[throwException]] is `true`.
40
     */
41
    public function parse($rawBody, $contentType)
42
    {
43
        $token = self::getTokenFromHash(Yii::$app->request->getHeaders()->get(self::HASH_HEADER, null));
44
        $nonce = Yii::$app->request->getHeaders()->get(self::NONCE_HEADER, null);
45
46
        try {
47
            if ($nonce === null) {
48
                // If a nonce is not provided, use crypto_box_seal
49
                $rawBody = $this->getRawBodyFromToken($token, $rawBody);
50
            } else {
51
                // Otherwise, decrypt the box using the nonce and key
52
                $rawBody = $this->getRawBodyFromTokenAndNonce($token, $nonce, $rawBody);
53
            }
54
55
            if ($rawBody === false) {
56
                throw new \Exception;
57
            }
58
59
        } catch (\Exception $e) {
60
            throw new BadRequestHttpException(Yii::t('yrc', 'Invalid security headers. Your session is either invalid or has expired.'));
61
        }
62
63
        try {
64
            $parameters = Json::decode($rawBody, $this->asArray);
65
            return $parameters === null ? [] : $parameters;
66
        } catch (InvalidParamException $e) {
67
            if ($this->throwException) {
68
                throw new BadRequestHttpException('Invalid JSON data in request body: ' . $e->getMessage());
69
            }
70
            return [];
71
        }
72
    }
73
74
    /**
75
     * Decrypts the raw body using TokenKeyPair, the client submitted nonce and crypto_box_open
76
     * @param TokenKeyPair $token
77
     * @param string $nonce
78
     * @param string $rawBody
79
     * @return string
80
     */
81
    private function getRawBodyFromTokenAndNonce($token, $nonce, $rawBody)
82
    {
83
        // Construct a keypair from the client_public and the server private key
84
        $kp = \Sodium\crypto_box_keypair_from_secretkey_and_publickey(
85
            \base64_decode($token->secret_box_kp),
86
            \base64_decode($token->client_public)
87
        );
88
89
        // Decrypt the raw body
90
        $rawBody = \Sodium\crypto_box_open(
91
            \base64_decode($rawBody),
92
            \base64_decode($nonce),
93
            $kp
94
        );
95
        
96
        return $rawBody;
97
    }
98
99
    /**
100
     * Decrypts the raw body using TokenKeyPair and crypto_box_seal_open
101
     * @param TokenKeyPair $token
102
     * @param string $rawBody
103
     * @return string
104
     */
105
    private function getRawBodyFromToken($token, $rawBody)
106
    {
107
        $rawBody = \Sodium\crypto_box_seal_open(
108
            \base64_decode($rawBody),
109
            $token->getBoxKeyPair()
110
        );
111
112
        // If this is a One Time Use Token, delete it to prevent it from being reused
113
        if ($token->type === TokenKeyPair::OTK_TYPE) {
114
            $token->delete();
115
        }
116
117
        return $rawBody;
118
    }
119
120
    /**
121
     * Helper method to retrieve and valdiate the token
122
     * @return Token
123
     */
124
    public static function getTokenFromHash($hash)
125
    {
126
        // Fetch the hash from the header
127
        if ($hash === null) {
128
            throw new BadRequestHttpException(Yii::t('yrc', 'Missing x-hashid header'));
129
        }
130
131
        $token = TokenKeyPair::find()->where(['hash' => $hash])->one();
132
133
        if ($token === null) {
134
            throw new BadRequestHttpException(Yii::t('yrc', 'Invalid x-hashid header. The provided header is either invalid or expired.'));
135
        }
136
137
        return $token;
138
    }
139
}