Completed
Push — master ( 206c33...9e77c2 )
by Oscar
05:44
created

CryptTrait   C

Complexity

Total Complexity 72

Size/Duplication

Total Lines 128
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 0

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 72
c 1
b 0
f 0
lcom 1
cbo 0
dl 0
loc 128
rs 5.5668

7 Methods

Rating   Name   Duplication   Size   Complexity  
B crypt() 0 13 5
A generateCryptKey() 0 4 1
A encrypt() 0 14 2
B decrypt() 0 21 5
A hash() 0 4 1
A invalidPayload() 0 4 4
A validMac() 0 7 1

How to fix   Complexity   

Complex Class

Complex classes like CryptTrait often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use CryptTrait, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Psr7Middlewares\Utils;
4
5
use RuntimeException;
6
7
/**
8
 * Trait used by all middlewares that needs encrypt/decrypt functions
9
 * Most of code is from https://github.com/illuminate/encryption.
10
 */
11
trait CryptTrait
12
{
13
    protected $cipher;
14
    protected $key;
15
16
    /**
17
     * Set the key and cipher used by the crypt.
18
     * 
19
     * @param string $key
20
     * @param string $cipher
21
     *
22
     * @return self
23
     */
24
    public function crypt($key, $cipher = 'AES-128-CBC')
25
    {
26
        $length = mb_strlen($key, '8bit');
27
28
        if (!(($cipher === 'AES-128-CBC' && $length === 16) || ($cipher === 'AES-256-CBC' && $length === 32))) {
29
            throw new RuntimeException('The only supported ciphers are AES-128-CBC and AES-256-CBC with the correct key lengths.');
30
        }
31
32
        $this->key = $key;
33
        $this->cipher = $cipher;
34
35
        return $this;
36
    }
37
38
    /**
39
     * Generates the default cipher and key.
40
     */
41
    protected function generateCryptKey()
42
    {
43
        $this->crypt(substr(md5(__DIR__), 0, 16));
44
    }
45
46
    /**
47
     * Encrypt the given value.
48
     *
49
     * @param string $value
50
     * 
51
     * @return string
52
     */
53
    protected function encrypt($value)
54
    {
55
        $iv = random_bytes(16);
0 ignored issues
show
Unused Code introduced by
The call to random_bytes() has too many arguments starting with 16.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
56
        $value = openssl_encrypt(serialize($value), $this->cipher, $this->key, 0, $iv);
57
58
        if ($value === false) {
59
            throw new RuntimeException('Could not encrypt the data.');
60
        }
61
62
        $iv = base64_encode($iv);
63
        $mac = hash_hmac('sha256', $iv.$value, $this->key);
64
65
        return base64_encode(json_encode(compact('iv', 'value', 'mac')));
66
    }
67
68
    /**
69
     * Decrypt the given value.
70
     *
71
     * @param string $payload
72
     * 
73
     * @return string
74
     */
75
    protected function decrypt($payload)
76
    {
77
        $payload = json_decode(base64_decode($payload), true);
78
79
        if (!$payload || $this->invalidPayload($payload)) {
80
            throw new RuntimeException('The payload is invalid.');
81
        }
82
83
        if (!$this->validMac($payload)) {
84
            throw new RuntimeException('The MAC is invalid.');
85
        }
86
87
        $iv = base64_decode($payload['iv']);
88
        $decrypted = openssl_decrypt($payload['value'], $this->cipher, $this->key, 0, $iv);
89
90
        if ($decrypted === false) {
91
            throw new RuntimeException('Could not decrypt the data.');
92
        }
93
94
        return unserialize($decrypted);
95
    }
96
97
    /**
98
     * Create a MAC for the given value.
99
     *
100
     * @param string $iv
101
     * @param string $value
102
     * 
103
     * @return string
104
     */
105
    protected function hash($iv, $value)
106
    {
107
        return hash_hmac('sha256', $iv.$value, $this->key);
108
    }
109
110
    /**
111
     * Verify that the encryption payload is valid.
112
     *
113
     * @param array|mixed $data
114
     * 
115
     * @return bool
116
     */
117
    protected function invalidPayload($data)
118
    {
119
        return !is_array($data) || !isset($data['iv']) || !isset($data['value']) || !isset($data['mac']);
120
    }
121
122
    /**
123
     * Determine if the MAC for the given payload is valid.
124
     *
125
     * @param array $payload
126
     * 
127
     * @throws \RuntimeException
128
     *
129
     * @return bool
130
     */
131
    protected function validMac(array $payload)
132
    {
133
        $bytes = random_bytes(16);
0 ignored issues
show
Unused Code introduced by
The call to random_bytes() has too many arguments starting with 16.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
134
        $calcMac = hash_hmac('sha256', $this->hash($payload['iv'], $payload['value']), $bytes, true);
135
136
        return hash_equals(hash_hmac('sha256', $payload['mac'], $bytes, true), $calcMac);
137
    }
138
}
139