EncryptionHelper::create_32bit_password()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 6
rs 10
1
<?php
2
3
namespace BiffBangPow\SSMonitor\Server\Helper;
4
5
use SilverStripe\Core\Config\Configurable;
6
use SilverStripe\Core\Environment;
7
8
class EncryptionHelper
9
{
10
11
    use Configurable;
12
13
    /**
14
     * @config
15
     * @var int $hashlength
16
     */
17
    private static int $hashlength = 50;
18
19
    /**
20
     * @var string $secret
21
     */
22
    private string $secret;
23
24
    /**
25
     * @var string $salt
26
     */
27
    private string $salt;
28
29
30
    public function __construct($encSecret, $encSalt)
31
    {
32
        if (!$encSalt || !$encSecret) {
33
            throw new \Exception("Missing encryption keys");
34
        }
35
36
        $this->setSecret($encSecret);
37
        $this->setSalt($encSalt);
38
    }
39
40
41
    public function encrypt($plaintext)
42
    {
43
44
        $secret = $this->getSecret();
45
46
        // Create a 32bit password
47
        $key = $this->create_32bit_password($secret);
48
49
        // Create a nonce: a piece of non-secret unique data that is used to randomize the cipher (safety against replay attack).
50
        // The nonce should be stored or shared along with the ciphertext, because the nonce needs to be reused with the same key.
51
        // In this class the nonce is shared with the ciphertext.
52
        $nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
53
54
        // Encrypted
55
        $ciphertext = bin2hex(
56
            sodium_crypto_secretbox($plaintext, $nonce, $key)
57
        );
58
59
        // Hex nonce (in order to send together with ciphertext)
60
        $nonce_hex = bin2hex($nonce);
61
62
        // Create hash from ciphertext+nonce
63
        // It is not necessary, but just an extra layer of defense:
64
        // - more difficult to manipulate the string
65
        // - a nonce is always 48 characters. Because of a trailing hash (of unkown length), the nonce cannot be identified easily.
66
        //   (a nonce does not have to be secret, this is just an extra precaution)
67
        $hash = $this->create_hash($ciphertext . $nonce_hex);
68
69
        // Return ciphertext + nonce + hash
70
        return $ciphertext . $nonce_hex . $hash;
71
    }
72
73
74
    public function decrypt($ciphertext)
75
    {
76
77
        $secret = $this->getSecret();
78
79
        // Create a 32bit password
80
        $key = $this->create_32bit_password($secret);
81
82
        //Get hash
83
        $hash = substr($ciphertext, -self::$hashlength);
84
85
        //Get ciphertext + nonce (remove trailing hash)
86
        $ciphertext = substr($ciphertext, 0, -self::$hashlength);
87
88
        //Re-create hash
89
        $hash_on_the_fly = $this->create_hash($ciphertext);
90
91
        //Check if hash is correct
92
        if ($hash !== $hash_on_the_fly) {
93
            //Do proper error handling
94
            return "error";
95
        } else {
96
            // Get nonce (last 48 chars of string)
97
            $nonce_hex = substr($ciphertext, -48);
98
99
            // Get ciphertext (remove nonce)
100
            $ciphertext = substr($ciphertext, 0, -48);
101
102
            // Bin nonce
103
            $nonce = hex2bin($nonce_hex);
104
105
            // Decrypted
106
            $plaintext = sodium_crypto_secretbox_open(
107
                hex2bin($ciphertext), $nonce, $key
108
            );
109
110
            return $plaintext;
111
        }
112
    }
113
114
115
    private function create_32bit_password($secret)
116
    {
117
        $salt = $this->getSalt();
118
119
        //Openlib needs a 32bit key for encryption
120
        return substr(bin2hex(sodium_crypto_generichash($secret . $salt)), 0, 32);
121
    }
122
123
124
    private function create_hash($ciphertext_and_nonce)
125
    {
126
        $hashlength = $this->config()->get('hashlength');
127
        return substr(bin2hex(sodium_crypto_generichash($ciphertext_and_nonce)), 0, $hashlength);
128
    }
129
130
    /**
131
     * @return mixed
132
     */
133
    public function getSecret()
134
    {
135
        return $this->secret;
136
    }
137
138
    /**
139
     * @param mixed $secret
140
     */
141
    public function setSecret($secret): void
142
    {
143
        $this->secret = $secret;
144
    }
145
146
    /**
147
     * @return mixed
148
     */
149
    public function getSalt()
150
    {
151
        return $this->salt;
152
    }
153
154
    /**
155
     * @param mixed $salt
156
     */
157
    public function setSalt($salt): void
158
    {
159
        $this->salt = $salt;
160
    }
161
162
    /**
163
     * @throws \Exception
164
     */
165
    public static function generateRandomString(int $length = 64): string
166
    {
167
        if ($length < 1) {
168
            throw new \RangeException("Length must be a positive integer");
169
        }
170
        $keyspace = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
171
        $pieces = [];
172
        $max = mb_strlen($keyspace, '8bit') - 1;
173
        for ($i = 0; $i < $length; ++$i) {
174
            $pieces [] = $keyspace[random_int(0, $max)];
175
        }
176
        return implode('', $pieces);
177
    }
178
}
179