Tag70Packet   A
last analyzed

Complexity

Total Complexity 22

Size/Duplication

Total Lines 292
Duplicated Lines 0 %

Test Coverage

Coverage 92.94%

Importance

Changes 0
Metric Value
dl 0
loc 292
ccs 79
cts 85
cp 0.9294
rs 10
c 0
b 0
f 0
wmc 22

6 Methods

Rating   Name   Duplication   Size   Complexity  
A createRandomPrefix() 0 18 2
A dump() 0 8 1
A __construct() 0 2 1
D decrypt() 0 42 9
B generate() 0 43 6
B parse() 0 27 3
1
<?php
2
3
/*
4
 * This file is part of the PHP EcryptFS library.
5
 * (c) 2017 by Dennis Birkholz
6
 * All rights reserved.
7
 * For the license to use this library, see the provided LICENSE file.
8
 */
9
10
namespace Iqb\Ecryptfs;
11
12
/**
13
 * FNEK-encrypted filename as dentry name (Tag 70)
14
 *
15
 * Structure according to Ecryptfs sources:
16
 * Octet 0: Tag 70 identifier
17
 * Octets 1-N1: Tag 70 packet size (includes cipher identifier
18
 *              and block-aligned encrypted filename size)
19
 * Octets N1-N2: FNEK sig (ECRYPTFS_SIG_SIZE)
20
 * Octet N2-N3: Cipher identifier (1 octet)
21
 * Octets N3-N4: Block-aligned encrypted filename
22
 *  - Consists of a minimum number of random numbers, a \0
23
 *    separator, and then the filename
24
 *
25
 * @author Dennis Birkholz <[email protected]>
26
 * @link https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git/tree/fs/ecryptfs/keystore.c?h=v4.11.3#n892 ecryptfs_parse_tag_70_packet
27
 * @link https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git/tree/fs/ecryptfs/keystore.c?h=v4.11.3#n614 ecryptfs_write_tag_70_packet
28
 */
29
final class Tag70Packet
30
{
31
    /**
32
     * @link https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git/tree/fs/ecryptfs/ecryptfs_kernel.h?h=v4.11.3#n146
33
     */
34
    const PACKET_TYPE = 0x46;
35
36
    /**
37
     * @link https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git/tree/fs/ecryptfs/ecryptfs_kernel.h?h=v4.11.3#n138 ECRYPTFS_TAG_70_DIGEST
38
     */
39
    const DIGEST = 'md5';
40
41
    /**
42
     * @link https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git/tree/fs/ecryptfs/ecryptfs_kernel.h?h=v4.11.3#n164 ECRYPTFS_TAG_70_DIGEST_SIZE
43
     */
44
    const DIGEST_SIZE = 16;
45
46
    /**
47
     * Minimum number of "Random" bytes (= derived with DIGEST from FNEK)
48
     *
49
     * This is intended to work as an IV but ECB is used so the IV is pointless after the first block ...
50
     *
51
     * @link https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git/tree/fs/ecryptfs/ecryptfs_kernel.h?h=v4.11.3#n161
52
     * @link https://defuse.ca/audits/ecryptfs.htm
53
     */
54
    const MIN_RANDOM_PREPEND_BYTES = 16;
55
56
    /**
57
     * Used cipher is stored in the packet so use something "strong" here ...
58
     */
59
    const DEFAULT_CIPHER = RFC2440_CIPHER_AES_256;
60
61
    /**
62
     * "A reasonable substitute for NULL"
63
     *
64
     * @link https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git/tree/fs/ecryptfs/ecryptfs_kernel.h?h=v4.11.3#n162
65
     */
66
    const NON_NULL = 0x42;
67
68
    /**
69
     * @var int
70
     */
71
    public $packetSize;
72
73
    /**
74
     * Signature of the key used to encrypt the payload
75
     * @var string
76
     */
77
    public $signature;
78
79
    /**
80
     * Total number of bytes the encrypted filename uses including padding
81
     * @var int
82
     */
83
    public $blockAlignedFilenameSize;
84
85
    /**
86
     * Numeric identifier of the used cipher, one of the RFC2440_CIPHER_* constants
87
     * @var int
88
     */
89
    public $cipherCode;
90
91
    /**
92
     * Key size in bytes
93
     *
94
     * @var int
95
     */
96
    public $cipherKeySize;
97
98
    /**
99
     * @var string
100
     */
101
    public $encryptedFilename;
102
103
    /**
104
     * @var string
105
     */
106
    public $decryptedFilename;
107
108
    /**
109
     * @var string
110
     */
111
    public $padding;
112
113
114
    /**
115
     * Prevent creation without proper initialization from factory methods
116
     */
117 36
    private function __construct()
118
    {
119 36
    }
120
121
122
    /**
123
     * Generate the binary representation of this packet
124
     */
125 36
    final public function dump() : string
126
    {
127
        return
128 36
            \chr(self::PACKET_TYPE)
129 36
            . Util::generateTagPacketLength($this->packetSize)
130 36
            . \hex2bin($this->signature)
131 36
            . \chr($this->cipherCode)
132 36
            . $this->encryptedFilename
133
            ;
134
    }
135
136
137
    /**
138
     * Decrypt the encrypted payload and extract padding and filename from it.
139
     *
140
     * A TAG70 packet does not contain the
141
     *
142
     * @param CryptoEngineInterface $cryptoEngine
143
     * @param string $fnek File name encryption key
144
     * @return string The decrypted filename
145
     *
146
     * @link https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git/tree/fs/ecryptfs/keystore.c?h=v4.11.3#n1051
147
     */
148 36
    final public function decrypt(CryptoEngineInterface $cryptoEngine, string $fnek) : string
149
    {
150 36
        if ($this->signature !== ($keySignature = Util::calculateSignature($fnek))) {
151
            throw new \InvalidArgumentException("Signature mismatch: require {$this->signature}, got $keySignature");
152
        }
153
154 36
        $blockSize = $cryptoEngine::CIPHER_BLOCK_SIZES[$this->cipherCode];
155 36
        $iv = \str_repeat("\0", $blockSize);
156 36
        $padding = self::createRandomPrefix($fnek, 8);
157
158 36
        $correctKeySize = false;
159 36
        $possibleCipherKeySizes = ($this->cipherKeySize ? [$this->cipherKeySize] : $cryptoEngine::CIPHER_KEY_SIZES[$this->cipherCode]);
160
161 36
        foreach ($possibleCipherKeySizes as $cipherKeySize) {
162 36
            $realKey = \substr($fnek, 0, $cipherKeySize);
163
164 36
            $decrypted = '';
165 36
            foreach (\str_split($this->encryptedFilename, $blockSize) as $blockNum => $block) {
166 36
                $decrypted .= $cryptoEngine->decrypt($block, $this->cipherCode, $realKey, $iv);
167
168
                // "Random" bytes do not match expected bytes, key or key size is wrong
169 36
                if ($blockNum === 0) {
170 36
                    if (\substr($decrypted, 0, 8) === $padding) {
171 36
                        $this->cipherKeySize = $cipherKeySize;
172 36
                        $correctKeySize = true;
173
                    } else {
174 36
                        continue 2;
175
                    }
176
                }
177
            }
178
179 36
            if ($correctKeySize) {
180 36
                break;
181
            }
182
        }
183
184 36
        if (!$correctKeySize) {
185
            throw new \RuntimeException(\sprintf("Unable to decrypt filename, filename encryption key (FNEK) invalid or invalid key length for cipher 0x%x, tested key sizes: (%s)", $this->cipherCode, \implode(', ', \array_map(function($bytes) { return $bytes*8; }, $possibleCipherKeySizes))));
186
        }
187
188 36
        list($this->padding, $this->decryptedFilename) = \explode("\0", $decrypted, 2);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $decrypted seems to be defined by a foreach iteration on line 161. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
189 36
        return $this->decryptedFilename;
190
    }
191
192
193
    /**
194
     * Generate a Tag70 packet from for the supplied plainText string.
195
     *
196
     * @param CryptoEngineInterface $cryptoEngine
197
     * @param string $plainText
198
     * @param string $fnek File name encryption key
199
     * @param int $cipherCode
200
     * @return Tag70Packet
201
     *
202
     * @link https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git/tree/fs/ecryptfs/keystore.c?h=v4.11.3#n614
203
     */
204 36
    public static function generate(CryptoEngineInterface $cryptoEngine, string $plainText, string $fnek, int $cipherCode = self::DEFAULT_CIPHER, int $cipherKeySize = null) : self
205
    {
206 36
        if ($cipherKeySize === null) {
207
            $cipherKeySize = Util::findCipherKeySize($cipherCode, \strlen($fnek));
208
        }
209
210 36
        elseif (\strlen($fnek) < $cipherKeySize) {
211
            throw new \InvalidArgumentException(\şprintf("Supplied key has only %u bytes but %u bytes required for encryption.", \strlen($$fnek), $cipherKeySize));
0 ignored issues
show
Bug introduced by
The function şprintf was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

211
            throw new \InvalidArgumentException(/** @scrutinizer ignore-call */ \şprintf("Supplied key has only %u bytes but %u bytes required for encryption.", \strlen($$fnek), $cipherKeySize));
Loading history...
212
        }
213
214 36
        elseif (!\in_array($cipherKeySize, CryptoEngineInterface::CIPHER_KEY_SIZES[$cipherCode])) {
215
            throw new \InvalidArgumentException(\şprintf("Requested key size %u bytes is unsupported for cipher 0x%x.", $cipherKeySize, $cipherCode));
216
        }
217
218 36
        $tag = new self();
219 36
        $tag->cipherCode = $cipherCode;
220 36
        $tag->signature = Util::calculateSignature($fnek);
221 36
        $tag->decryptedFilename = $plainText;
222
223 36
        $blockSize = $cryptoEngine::CIPHER_BLOCK_SIZES[$cipherCode];
224
225
        // Calculate length of the encoded file name and required "random" prefix
226 36
        $filenameSize = \strlen($tag->decryptedFilename);
227 36
        $randomPrefixSize = self::MIN_RANDOM_PREPEND_BYTES;
228 36
        $tag->blockAlignedFilenameSize = $randomPrefixSize + 1 + $filenameSize;
229 36
        if ($tag->blockAlignedFilenameSize % $blockSize > 0) {
230 36
            $randomPrefixSize += $blockSize - ($tag->blockAlignedFilenameSize % $blockSize);
231 36
            $tag->blockAlignedFilenameSize = $randomPrefixSize + 1 + $filenameSize;
232
        }
233 36
        $tag->packetSize = ECRYPTFS_SIG_SIZE + 1 + $tag->blockAlignedFilenameSize;
234
235
        // The actual padded filename contains the prefix separated by \0 from the plain text filename
236 36
        $tag->padding = self::createRandomPrefix($fnek, $randomPrefixSize);
237 36
        $paddedFilename = $tag->padding . "\0" . $tag->decryptedFilename;
238
239 36
        $realKey = \substr($fnek, 0, $cipherKeySize);
240 36
        $tag->encryptedFilename = '';
241 36
        $iv = \str_repeat("\0", $blockSize);
242 36
        foreach (\str_split($paddedFilename, $blockSize) as $block) {
243 36
            $tag->encryptedFilename .= $cryptoEngine->encrypt($block, $tag->cipherCode, $realKey, $iv);
244
        }
245
246 36
        return $tag;
247
    }
248
249
250
    /**
251
     * Generate the "random" prefix prepended to the filename before encryption.
252
     *
253
     * The "random" prefix is not that random, it is created from the MD5 sum of the FNEK
254
     * The prefix is a substring of md5($fnek).md5(md5($fnek)).
255
     *
256
     * @param string $fnek
257
     * @param int $requiredBytes
258
     * @return string
259
     *
260
     * @link https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git/tree/fs/ecryptfs/keystore.c?h=v4.11.3#n786
261
     */
262 36
    private static function createRandomPrefix(string $fnek, int $requiredBytes) : string
263
    {
264 36
        $prefix = '';
265 36
        $hash = $fnek;
266
267 36
        for ($i=0; $i<\ceil($requiredBytes / self::DIGEST_SIZE); $i++) {
268 36
            $hash = \hash(self::DIGEST, $hash, true);
269 36
            $prefix .= $hash;
270
        }
271
272
        // Use only required bytes
273 36
        $prefix = \substr($prefix, 0, $requiredBytes);
274
275
        // Replace \0 because we separate prefix and actual file name with \0
276
        // @link https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git/tree/fs/ecryptfs/keystore.c?h=v4.11.3#n786
277 36
        $prefix = \str_replace("\0", \chr(self::NON_NULL), $prefix);
278
279 36
        return $prefix;
280
    }
281
282
283
    /**
284
     * Try to parse a Tag70 packet from the supplied data string.
285
     * Call decrypt() afterwards to actually decrypt the filename
286
     * If the parsing was successfully, $pos will be incremented to point after the parsed data.
287
     *
288
     * @param string $data
289
     * @param int $pos
290
     * @return Tag70Packet
291
     *
292
     * @link https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git/tree/fs/ecryptfs/keystore.c?h=v4.11.3#n892
293
     */
294 36
    public static function parse(string $data, int &$pos = 0) : self
295
    {
296 36
        $cur = $pos;
297 36
        $tag = new self();
298
299 36
        if (\ord($data[$cur]) !== self::PACKET_TYPE) {
300
            throw new \DomainException("Expected packet type marker 0x" . \bin2hex(self::PACKET_TYPE) . " but found 0x" . \bin2hex(\ord($data[$cur])));
301
        }
302 36
        $cur++;
303
304 36
        $tag->packetSize = Util::parseTagPacketLength($data, $cur);
305
306 36
        $tag->signature = \bin2hex(\substr($data, $cur, ECRYPTFS_SIG_SIZE));
307 36
        $cur += ECRYPTFS_SIG_SIZE;
308
309 36
        $tag->cipherCode = \ord($data[$cur]);
310 36
        if (!\array_key_exists($tag->cipherCode, CryptoEngineInterface::CIPHER_BLOCK_SIZES)) {
311
            throw new \DomainException('Invalid cipher type 0x' . \dechex($tag->cipherCode));
312
        }
313 36
        $cur++;
314
315 36
        $tag->blockAlignedFilenameSize = $tag->packetSize - ECRYPTFS_SIG_SIZE - 1;
316 36
        $tag->encryptedFilename = \substr($data, $cur, $tag->blockAlignedFilenameSize);
317 36
        $cur += $tag->blockAlignedFilenameSize;
318
319 36
        $pos = $cur;
320 36
        return $tag;
321
    }
322
}
323