AESKW   A
last analyzed

Complexity

Total Complexity 30

Size/Duplication

Total Lines 173
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 0

Importance

Changes 0
Metric Value
wmc 30
lcom 1
cbo 0
dl 0
loc 173
rs 10
c 0
b 0
f 0

10 Methods

Rating   Name   Duplication   Size   Complexity  
A wrap() 0 27 5
B unwrap() 0 33 6
A getInitialValue() 0 14 2
B checkInitialValue() 0 34 8
A checkKeySize() 0 9 4
A toXBits() 0 4 1
A getMSB() 0 4 1
A getLSB() 0 4 1
A encrypt() 0 4 1
A decrypt() 0 4 1
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * The MIT License (MIT)
7
 *
8
 * Copyright (c) 2014-2020 Spomky-Labs
9
 *
10
 * This software may be modified and distributed under the terms
11
 * of the MIT license.  See the LICENSE file for details.
12
 */
13
14
namespace AESKW;
15
16
use function count;
17
use InvalidArgumentException;
18
use function Safe\hex2bin;
19
use function Safe\mb_str_split;
20
use function Safe\openssl_decrypt;
21
use function Safe\openssl_encrypt;
22
23
trait AESKW
24
{
25
    /**
26
     * @param string $kek             The Key Encryption Key
27
     * @param string $key             The key to wrap
28
     * @param bool   $padding_enabled If false, the key to wrap must be a sequence of one or more 64-bit blocks (RFC3394 compliant), else the key size must be at least one octet (RFC5649 compliant)
29
     *
30
     * @return string The wrapped key
31
     */
32
    public static function wrap(string $kek, string $key, bool $padding_enabled = false): string
33
    {
34
        $A = self::getInitialValue($key, $padding_enabled);
35
        self::checkKeySize($key, $padding_enabled);
36
        $P = mb_str_split($key, 8, '8bit');
37
        $N = count($P);
38
        $C = [];
39
40
        if (1 === $N) {
41
            $B = self::encrypt($kek, $A.$P[0]);
42
            $C[0] = self::getMSB($B);
43
            $C[1] = self::getLSB($B);
44
        } elseif (1 < $N) {
45
            $R = $P;
46
            for ($j = 0; $j <= 5; ++$j) {
47
                for ($i = 1; $i <= $N; ++$i) {
48
                    $B = self::encrypt($kek, $A.$R[$i - 1]);
49
                    $t = $i + $j * $N;
50
                    $A = self::toXBits(64, $t) ^ self::getMSB($B);
51
                    $R[$i - 1] = self::getLSB($B);
52
                }
53
            }
54
            $C = array_merge([$A], $R);
55
        }
56
57
        return implode('', $C);
58
    }
59
60
    /**
61
     * @param string $kek             The Key Encryption Key
62
     * @param string $key             The key to unwrap
63
     * @param bool   $padding_enabled If false, the AIV check must be RFC3394 compliant, else it must be RFC5649 or RFC3394 compliant
64
     *
65
     * @return string The key unwrapped
66
     */
67
    public static function unwrap(string $kek, string $key, bool $padding_enabled = false): string
68
    {
69
        $P = mb_str_split($key, 8, '8bit');
70
        $A = $P[0];
71
        $N = count($P);
72
73
        if (2 > $N) {
74
            throw new InvalidArgumentException('Bad data');
75
        }
76
        if (2 === $N) {
77
            $B = self::decrypt($kek, $P[0].$P[1]);
78
            $unwrapped = self::getLSB($B);
79
            $A = self::getMSB($B);
80
        } else {
81
            $R = $P;
82
            for ($j = 5; $j >= 0; --$j) {
83
                for ($i = $N - 1; $i >= 1; --$i) {
84
                    $t = $i + $j * ($N - 1);
85
                    $B = self::decrypt($kek, (self::toXBits(64, $t) ^ $A).$R[$i]);
86
                    $A = self::getMSB($B);
87
                    $R[$i] = self::getLSB($B);
88
                }
89
            }
90
            unset($R[0]);
91
92
            $unwrapped = implode('', $R);
93
        }
94
        if (false === self::checkInitialValue($unwrapped, $padding_enabled, $A)) {
95
            throw new InvalidArgumentException('Integrity check failed!');
96
        }
97
98
        return $unwrapped;
99
    }
100
101
    /**
102
     * The initial value used to wrap the key and check the integrity when unwrapped.
103
     * The RFC3394 set this value to 0xA6A6A6A6A6A6A6A6
104
     * The RFC5649 set this value to 0xA65959A6XXXXXXXX (The part with XXXXXXXX is the MLI, depends on the padding).
105
     *
106
     * @param string $key             The key
107
     * @param bool   $padding_enabled Enable padding (RFC5649)
108
     *
109
     * @see https://tools.ietf.org/html/rfc3394#section-2.2.3.1
110
     */
111
    private static function getInitialValue(string &$key, bool $padding_enabled): string
112
    {
113
        if (false === $padding_enabled) {
114
            return hex2bin('A6A6A6A6A6A6A6A6');
115
        }
116
117
        $MLI = mb_strlen($key, '8bit');
118
        $iv = hex2bin('A65959A6').self::toXBits(32, $MLI);
119
120
        $n = (int) ceil($MLI / 8);
121
        $key = str_pad($key, 8 * $n, "\0", STR_PAD_RIGHT);
122
123
        return $iv;
124
    }
125
126
    private static function checkInitialValue(string &$key, bool $padding_enabled, string $iv): bool
127
    {
128
        // RFC3394 compliant
129
        if ($iv === hex2bin('A6A6A6A6A6A6A6A6')) {
130
            return true;
131
        }
132
133
        // The RFC3394 is required but the previous check is not satisfied => invalid
134
        if (false === $padding_enabled) {
135
            return false;
136
        }
137
138
        // The high-order half of the AIV according to the RFC5649
139
        if (hex2bin('A65959A6') !== self::getMSB($iv)) {
140
            return false;
141
        }
142
143
        $n = mb_strlen($key, '8bit') / 8;
144
        $MLI = (int) hexdec(bin2hex(ltrim(self::getLSB($iv), "\0")));
145
146
        if (!(8 * ($n - 1) < $MLI && $MLI <= 8 * $n)) {
147
            return false;
148
        }
149
150
        $b = 8 * $n - $MLI;
151
        for ($i = 0; $i < $b; ++$i) {
152
            if ("\0" !== mb_substr($key, $MLI + $i, 1, '8bit')) {
153
                return false;
154
            }
155
        }
156
        $key = mb_substr($key, 0, $MLI, '8bit');
157
158
        return true;
159
    }
160
161
    private static function checkKeySize(string $key, bool $padding_enabled): void
162
    {
163
        if ('' === $key) {
164
            throw new InvalidArgumentException('Bad key size');
165
        }
166
        if (false === $padding_enabled && 0 !== mb_strlen($key, '8bit') % 8) {
167
            throw new InvalidArgumentException('Bad key size');
168
        }
169
    }
170
171
    private static function toXBits(int $bits, int $value): string
172
    {
173
        return hex2bin(str_pad(dechex($value), $bits / 4, '0', STR_PAD_LEFT));
174
    }
175
176
    private static function getMSB(string $value): string
177
    {
178
        return mb_substr($value, 0, mb_strlen($value, '8bit') / 2, '8bit');
179
    }
180
181
    private static function getLSB(string $value): string
182
    {
183
        return mb_substr($value, mb_strlen($value, '8bit') / 2, null, '8bit');
184
    }
185
186
    private static function encrypt(string $kek, string $data): string
187
    {
188
        return openssl_encrypt($data, self::getMethod(), $kek, OPENSSL_ZERO_PADDING | OPENSSL_RAW_DATA);
189
    }
190
191
    private static function decrypt(string $kek, string $data): string
192
    {
193
        return openssl_decrypt($data, self::getMethod(), $kek, OPENSSL_ZERO_PADDING | OPENSSL_RAW_DATA);
194
    }
195
}
196