FileHeader   A
last analyzed

Complexity

Total Complexity 9

Size/Duplication

Total Lines 153
Duplicated Lines 0 %

Test Coverage

Coverage 83.33%

Importance

Changes 0
Metric Value
dl 0
loc 153
ccs 30
cts 36
cp 0.8333
rs 10
c 0
b 0
f 0
wmc 9

2 Methods

Rating   Name   Duplication   Size   Complexity  
A decryptFileKey() 0 18 3
B parse() 0 41 6
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
 * The FileHeader class represents the metadata stored at the beginning of each encrypted file.
14
 * It marks the file as a valid EcryptFS encrypted file (via the Magic Marker) and
15
 * contains information about the real file size of the encrypted file, the key to decrypt the file,
16
 * the method used for encrypting the file, etc.
17
 */
18
class FileHeader
19
{
20
    /**
21
     * Default file header size
22
     *
23
     * @link https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git/tree/fs/ecryptfs/ecryptfs_kernel.h?h=v4.11.3#n45
24
     */
25
    const MINIMUM_HEADER_EXTENT_SIZE = 8192;
26
27
    /**
28
     * Default block size
29
     *
30
     * @link https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git/tree/fs/ecryptfs/ecryptfs_kernel.h?h=v4.11.3#n44
31
     */
32
    const DEFAULT_EXTENT_SIZE = 4096;
33
34
    /**
35
     * Magic marker to detect if a file is a valid EcryptFS file
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#n130
38
     */
39
    const MAGIC_MARKER = 0x3c81b7f5;
40
41
    /**
42
     * Size of the unencrypted file
43
     *
44
     * @var int
45
     */
46
    public $size;
47
48
    /**
49
     * @var int
50
     */
51
    public $version;
52
53
    /**
54
     * @var int
55
     */
56
    public $flags;
57
58
    /**
59
     * @var int
60
     */
61
    public $extentSize;
62
63
    /**
64
     * @var int
65
     */
66
    public $extentsAtFront;
67
68
    /**
69
     * @var int
70
     */
71
    public $metadataSize;
72
73
    /**
74
     * @var int
75
     */
76
    public $cipherCode;
77
78
    /**
79
     * Encrypted file encryption key (FEK), encrypted with the FEKEK
80
     *
81
     * @var string
82
     */
83
    public $encryptedFileKey;
84
85
    /**
86
     * Decrypted file encryption key (FEK)
87
     *
88
     * @var string
89
     */
90
    public $fileKey;
91
92
    /**
93
     * Root initialisation vector, used to calculate IV for each block.
94
     *
95
     * @var string
96
     */
97
    public $rootIv;
98
99
100 36
    public static function parse($fileHandle) : self
101
    {
102 36
        if (!\is_resource($fileHandle)) {
103
            throw new \InvalidArgumentException('Parameter $fileHandle must be an open file handle.');
104
        }
105
106 36
        $headerData = \stream_get_contents($fileHandle, self::MINIMUM_HEADER_EXTENT_SIZE);
107 36
        if (\strlen($headerData) < self::MINIMUM_HEADER_EXTENT_SIZE) {
108
            throw new \RuntimeException('Could not read enough data to parse header.');
109
        }
110
111 36
        $headerValues = unpack('Jsize/N2marker/Cversion/sreserved/Cflags/Nextentsize/nextentsatfront', $headerData);
112 36
        $pos = 26;
113
114
        // see https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git/tree/fs/ecryptfs/crypto.c?h=v4.11.3#n857
115 36
        if (($headerValues['marker1'] ^ self::MAGIC_MARKER) !== $headerValues['marker2']) {
116
            throw new \DomainException('Invalid magic marker.');
117
        }
118
119 36
        $header = new self();
120 36
        $header->size           = $headerValues['size'];
121 36
        $header->version        = $headerValues['version'];
122 36
        $header->flags          = $headerValues['flags'];
123 36
        $header->extentSize     = $headerValues['extentsize'];
124 36
        $header->extentsAtFront = $headerValues['extentsatfront'];
125 36
        $header->metadataSize   = $header->extentsAtFront * $header->extentSize;
126
127
        // Read remaining header data so stream is positioned at the beginning of the data
128 36
        if ($header->metadataSize > self::MINIMUM_HEADER_EXTENT_SIZE) {
129
            $headerData .= \stream_get_contents($fileHandle, ($header->metadataSize - self::MINIMUM_HEADER_EXTENT_SIZE));
130
        }
131
132 36
        $tag3 = Tag3Packet::parse($headerData, $pos);
133 36
        $header->cipherCode = $tag3->cipherCode;
134 36
        $header->encryptedFileKey = $tag3->encryptedKey;
135
136 36
        if (!\in_array(\strlen($header->encryptedFileKey), CryptoEngineInterface::CIPHER_KEY_SIZES[$header->cipherCode])) {
137
            throw new \RuntimeException(\sprintf("Invalid key size (%u bit) for cipher 0x%x detected, file header may be corrupt!", \strlen($header->encryptedFileKey)*8, $header->cipherCode));
138
        }
139
140 36
        return $header;
141
    }
142
143
144
    /**
145
     * Decrypt the file encryption key (FEK) using the file encryption key encryption key (FEKEK).
146
     * The cipher method for FEK encryption and for file contents encryption is encoded in the header.
147
     * The cipher key size for FEK encryption is the same as for file contents encryption so
148
     *  the same number of bytes from the FEKEK is uses as bytes for the FEK exist.
149
     *
150
     * @param CryptoEngineInterface $cryptoEngine
151
     * @param string $fekek
152
     */
153 36
    public function decryptFileKey(CryptoEngineInterface $cryptoEngine, string $fekek)
154
    {
155 36
        $cipherKeySize = \strlen($this->encryptedFileKey);
156 36
        if (\strlen($fekek) < $cipherKeySize) {
157
            throw new \InvalidArgumentException(\sprintf("Decryption requires %u key bytes, supplied FEKEK has only %u bytes!", $cipherKeySize, \strlen($fekek)));
158
        }
159 36
        $realFekek = \substr($fekek, 0, $cipherKeySize);
160
161 36
        $blockSize = $cryptoEngine::CIPHER_BLOCK_SIZES[$this->cipherCode];
162 36
        $iv = \str_repeat("\0", $blockSize);
163
164
        // Emulate ECB mode here ...
165 36
        $this->fileKey = '';
166 36
        foreach (\str_split($this->encryptedFileKey, $blockSize) as $block) {
167 36
            $this->fileKey .= $cryptoEngine->decrypt($block, $this->cipherCode, $realFekek, $iv);
168
        }
169
170 36
        $this->rootIv = \hash('md5', $this->fileKey, true);
171 36
    }
172
}
173