| 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
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
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
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 |