Issues (11)

src/Util.php (2 issues)

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
abstract class Util
13
{
14
    /**
15
     * The salt used for creating the file encryption key encryption key from the passphrase, unhex to use it.
16
     *
17
     * @link http://bazaar.launchpad.net/~ecryptfs/ecryptfs/trunk/view/head:/src/include/ecryptfs.h#L75
18
     * @link http://bazaar.launchpad.net/~ecryptfs/ecryptfs/trunk/view/head:/src/utils/ecryptfs_add_passphrase.c#L83
19
     */
20
    const DEFAULT_SALT_HEX = "0011223344556677";
21
22
    /**
23
     * The salt used for creating the file name encryption key from the passphrase.
24
     * Due to a programming error in the original ecryptfs user space library,
25
     * the salt is not unhexed before use.
26
     *
27
     * As a result only the first half (99887766) is used literally.
28
     *
29
     * @link http://bazaar.launchpad.net/~ecryptfs/ecryptfs/trunk/view/head:/src/include/ecryptfs.h#L76
30
     * @link http://bazaar.launchpad.net/~ecryptfs/ecryptfs/trunk/view/head:/src/utils/ecryptfs_add_passphrase.c#L108
31
     */
32
    const DEFAULT_SALT_FNEK_HEX = "9988776655443322";
33
34
    /**
35
     * Algorith used to generate keys from the supplied passphrase
36
     *
37
     * @link http://bazaar.launchpad.net/~ecryptfs/ecryptfs/trunk/view/head:/src/libecryptfs/main.c#L220
38
     */
39
    const KEY_DERIVATION_ALGO = "sha512";
40
41
    /**
42
     * Number of iterations when deriving the keys from the passphrase
43
     *
44
     * @link http://bazaar.launchpad.net/~ecryptfs/ecryptfs/trunk/view/head:/src/include/ecryptfs.h#L130
45
     * @link http://bazaar.launchpad.net/~ecryptfs/ecryptfs/trunk/view/head:/src/libecryptfs/main.c#L223
46
     */
47
    const DEFAULT_NUM_HASH_ITERATIONS = 65536;
48
49
    /**
50
     * Filename prefix for encrypted file names
51
     *
52
     * @link https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git/tree/fs/ecryptfs/ecryptfs_kernel.h?h=v4.11.3#n171
53
     */
54
    const FNEK_ENCRYPTED_FILENAME_PREFIX = 'ECRYPTFS_FNEK_ENCRYPTED.';
55
56
    /**
57
     * Derive a key from the supplied passphrase and salt
58
     *
59
     * @param string $passPhrase
60
     * @param string $salt
61
     * @param bool $hexEncode Whether to return the key hex encoded or not
62
     * @return string
63
     */
64 36
    final public static function deriveKey(string $passPhrase, string $salt, bool $hexEncode = false) : string
65
    {
66 36
        $key = \hash(self::KEY_DERIVATION_ALGO, \substr($salt, 0, ECRYPTFS_SALT_SIZE) . $passPhrase, true);
67
68 36
        for ($i=1; $i<self::DEFAULT_NUM_HASH_ITERATIONS; $i++) {
69 36
            $key = \hash(self::KEY_DERIVATION_ALGO, $key, true);
70
        }
71
72 36
        return ($hexEncode ? \bin2hex($key) : $key);
73
    }
74
75
    /**
76
     * Derive the file encryption key encrytion key (FEKEK) from the supplied passphrase.
77
     *
78
     * @param string $passPhrase
79
     * @param bool $hexEncode Whether to return the key hex encoded or not
80
     * @return string Derived key
81
     */
82 36
    final public static function deriveFEKEK(string $passPhrase, bool $hexEncode = false) : string
83
    {
84 36
        return self::deriveKey($passPhrase, \hex2bin(self::DEFAULT_SALT_HEX), $hexEncode);
85
    }
86
87
    /**
88
     * Derive the file encryption key encrytion key (FEKEK) from the supplied passphrase.
89
     *
90
     * @param string $passPhrase
91
     * @param bool $hexEncode Whether to return the key hex encoded or not
92
     * @return string Derived key
93
     */
94 36
    final public static function deriveFNEK(string $passPhrase, bool $hexEncode = false) : string
95
    {
96
        // Due to a programming error in the original Ecryptfs source code
97
        // the value of the ECRYPTFS_DEFAULT_SALT_FNEK_HEX is not hex decoded
98
        // but truncated and used without conversion.
99 36
        return self::deriveKey($passPhrase, \substr(self::DEFAULT_SALT_FNEK_HEX, 0, 8), $hexEncode);
100
    }
101
102
    /**
103
     * Calculate the signature of a key
104
     *
105
     * @param string $key Raw binary blob
106
     * @return string Hex encoded signature
107
     */
108 36
    final public static function calculateSignature(string $key) : string
109
    {
110 36
        return \bin2hex(\substr(\hash(self::KEY_DERIVATION_ALGO, $key, true), 0, ECRYPTFS_SIG_SIZE));
111
    }
112
113
114
    /**
115
     * Try to read the length of a packet from the supplied data.
116
     * On success, increases $pos to point to the next byte after the length
117
     *
118
     * @param string $data
119
     * @param int $pos
120
     * @return int
121
     * @throws ParseException
122
     */
123 51
    final public static function parseTagPacketLength(string $data, int &$pos = 0) : int
124
    {
125 51
        $packetSize = \ord($data[$pos]);
126 51
        if ($packetSize > 224) {
127 1
            throw new ParseException("Error parsing packet length!");
128
        }
129 50
        $pos++;
130
131
        // Read next byte from data
132 50
        if ($packetSize >= 192) {
133 1
            $packetSize = ($packetSize - 192) * 256;
134 1
            $packetSize += \ord($data[$pos++]);
135
        }
136
137 50
        return $packetSize;
138
    }
139
140
141
    /**
142
     * Generate the binary string representing the supplied length
143
     *
144
     * @param int $length
145
     * @return string
146
     */
147 38
    final public static function generateTagPacketLength(int $length) : string
148
    {
149 38
        if ($length < 0) {
150
            throw new \InvalidArgumentException("Length must be an unsigned integer.");
151
        }
152
153 38
        if ($length > (32*256 + 255)) {
154
            throw new \InvalidArgumentException("Length too large.");
155
        }
156
157 38
        if ($length < 192) {
158 37
            return \chr($length);
159
        }
160
161 1
        $low = $length % 256;
162 1
        $high = \floor($length / 256);
163
164 1
        return \chr($high + 192) . \chr($low);
0 ignored issues
show
$high + 192 of type double is incompatible with the type integer expected by parameter $ascii of chr(). ( Ignorable by Annotation )

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

164
        return \chr(/** @scrutinizer ignore-type */ $high + 192) . \chr($low);
Loading history...
165
    }
166
167
168
    /**
169
     * Check whether the supplied filename is encrypted
170
     *
171
     * @param string $filename
172
     * @return bool
173
     */
174 36
    public static function isEncryptedFilename(string $filename) : bool
175
    {
176 36
        return (\substr(\basename($filename), 0, \strlen(self::FNEK_ENCRYPTED_FILENAME_PREFIX )) === self::FNEK_ENCRYPTED_FILENAME_PREFIX);
177
    }
178
179
180
    /**
181
     * Encrypt the supplied filename
182
     *
183
     * @param CryptoEngineInterface $cryptoEngine
184
     * @param string $filename
185
     * @param string $fnek
186
     * @param int $cipherCode
187
     * @param int|null $cipherKeySize
188
     * @return string
189
     *
190
     * @link https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git/tree/fs/ecryptfs/crypto.c?h=v4.11.3#n1498
191
     */
192 36
    public static function encryptFilename(CryptoEngineInterface $cryptoEngine, string $filename, string $fnek, int $cipherCode = Tag70Packet::DEFAULT_CIPHER, int $cipherKeySize = null) : string
193
    {
194 36
        $tag = Tag70Packet::generate($cryptoEngine, $filename, $fnek, $cipherCode, $cipherKeySize);
195 36
        return self::FNEK_ENCRYPTED_FILENAME_PREFIX  . BaseConverter::encode($tag->dump());
196
    }
197
198
199
    /**
200
     * Decrypt the supplied filename
201
     */
202 36
    public static function decryptFilename(CryptoEngineInterface $cryptoEngine, string $filename, string $key) : string
203
    {
204 36
        if (!self::isEncryptedFilename($filename)) {
205
            return $filename;
206
        }
207
208 36
        $dirname = \dirname($filename);
209 36
        $decoded = BaseConverter::decode(\substr(\basename($filename), \strlen(self::FNEK_ENCRYPTED_FILENAME_PREFIX )));
210 36
        $tag = Tag70Packet::parse($decoded);
211 36
        $tag->decrypt($cryptoEngine, $key);
212
213 36
        return ($dirname && $dirname != '.' ? $dirname . '/' : '') . $tag->decryptedFilename;
214
    }
215
216
217
    /**
218
     * Find the largest possible cipher key size for the given cipher and key length
219
     *
220
     * @param int $cipherCode
221
     * @param int $keyLength
222
     * @return mixed
223
     */
224
    public static function findCipherKeySize(int $cipherCode, int $keyLength)
225
    {
226
        foreach (CryptoEngineInterface::CIPHER_KEY_SIZES[$cipherCode] as $possibleCipherKeySize) {
227
            if ($possibleCipherKeySize <= $keyLength) {
228
                $cipherKeySize = $possibleCipherKeySize;
229
                break;
230
            }
231
        }
232
233
        if (!isset($cipherKeySize)) {
234
            throw new \RuntimeException("Supplied key has only %u bytes, not enough for cipher 0x%x", $keyLength, $cipherCode);
0 ignored issues
show
$cipherCode of type integer is incompatible with the type Throwable|null expected by parameter $previous of RuntimeException::__construct(). ( Ignorable by Annotation )

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

234
            throw new \RuntimeException("Supplied key has only %u bytes, not enough for cipher 0x%x", $keyLength, /** @scrutinizer ignore-type */ $cipherCode);
Loading history...
235
        }
236
237
        return $cipherKeySize;
238
    }
239
}
240