You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

149 lines
4.4 KiB

3 years ago
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * @copyright Copyright (c) 2016, ownCloud, Inc.
  5. *
  6. * @author Andreas Fischer <bantu@owncloud.com>
  7. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  8. * @author Lukas Reschke <lukas@statuscode.ch>
  9. * @author Morris Jobke <hey@morrisjobke.de>
  10. * @author Roeland Jago Douma <roeland@famdouma.nl>
  11. *
  12. * @license AGPL-3.0
  13. *
  14. * This code is free software: you can redistribute it and/or modify
  15. * it under the terms of the GNU Affero General Public License, version 3,
  16. * as published by the Free Software Foundation.
  17. *
  18. * This program is distributed in the hope that it will be useful,
  19. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  20. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  21. * GNU Affero General Public License for more details.
  22. *
  23. * You should have received a copy of the GNU Affero General Public License, version 3,
  24. * along with this program. If not, see <http://www.gnu.org/licenses/>
  25. *
  26. */
  27. namespace OC\Security;
  28. use OCP\IConfig;
  29. use OCP\Security\ICrypto;
  30. use OCP\Security\ISecureRandom;
  31. use phpseclib\Crypt\AES;
  32. use phpseclib\Crypt\Hash;
  33. /**
  34. * Class Crypto provides a high-level encryption layer using AES-CBC. If no key has been provided
  35. * it will use the secret defined in config.php as key. Additionally the message will be HMAC'd.
  36. *
  37. * Usage:
  38. * $encryptWithDefaultPassword = \OC::$server->getCrypto()->encrypt('EncryptedText');
  39. * $encryptWithCustompassword = \OC::$server->getCrypto()->encrypt('EncryptedText', 'password');
  40. *
  41. * @package OC\Security
  42. */
  43. class Crypto implements ICrypto {
  44. /** @var AES $cipher */
  45. private $cipher;
  46. /** @var int */
  47. private $ivLength = 16;
  48. /** @var IConfig */
  49. private $config;
  50. /**
  51. * @param IConfig $config
  52. * @param ISecureRandom $random
  53. */
  54. public function __construct(IConfig $config) {
  55. $this->cipher = new AES();
  56. $this->config = $config;
  57. }
  58. /**
  59. * @param string $message The message to authenticate
  60. * @param string $password Password to use (defaults to `secret` in config.php)
  61. * @return string Calculated HMAC
  62. */
  63. public function calculateHMAC(string $message, string $password = ''): string {
  64. if ($password === '') {
  65. $password = $this->config->getSystemValue('secret');
  66. }
  67. // Append an "a" behind the password and hash it to prevent reusing the same password as for encryption
  68. $password = hash('sha512', $password . 'a');
  69. $hash = new Hash('sha512');
  70. $hash->setKey($password);
  71. return $hash->hash($message);
  72. }
  73. /**
  74. * Encrypts a value and adds an HMAC (Encrypt-Then-MAC)
  75. * @param string $plaintext
  76. * @param string $password Password to encrypt, if not specified the secret from config.php will be taken
  77. * @return string Authenticated ciphertext
  78. */
  79. public function encrypt(string $plaintext, string $password = ''): string {
  80. if ($password === '') {
  81. $password = $this->config->getSystemValue('secret');
  82. }
  83. $this->cipher->setPassword($password);
  84. $iv = \random_bytes($this->ivLength);
  85. $this->cipher->setIV($iv);
  86. $ciphertext = bin2hex($this->cipher->encrypt($plaintext));
  87. $iv = bin2hex($iv);
  88. $hmac = bin2hex($this->calculateHMAC($ciphertext.$iv, $password));
  89. return $ciphertext.'|'.$iv.'|'.$hmac.'|2';
  90. }
  91. /**
  92. * Decrypts a value and verifies the HMAC (Encrypt-Then-Mac)
  93. * @param string $authenticatedCiphertext
  94. * @param string $password Password to encrypt, if not specified the secret from config.php will be taken
  95. * @return string plaintext
  96. * @throws \Exception If the HMAC does not match
  97. * @throws \Exception If the decryption failed
  98. */
  99. public function decrypt(string $authenticatedCiphertext, string $password = ''): string {
  100. if ($password === '') {
  101. $password = $this->config->getSystemValue('secret');
  102. }
  103. $this->cipher->setPassword($password);
  104. $parts = explode('|', $authenticatedCiphertext);
  105. $partCount = \count($parts);
  106. if ($partCount < 3 || $partCount > 4) {
  107. throw new \Exception('Authenticated ciphertext could not be decoded.');
  108. }
  109. $ciphertext = hex2bin($parts[0]);
  110. $iv = $parts[1];
  111. $hmac = hex2bin($parts[2]);
  112. if ($partCount === 4) {
  113. $version = $parts[3];
  114. if ($version === '2') {
  115. $iv = hex2bin($iv);
  116. }
  117. }
  118. $this->cipher->setIV($iv);
  119. if (!hash_equals($this->calculateHMAC($parts[0] . $parts[1], $password), $hmac)) {
  120. throw new \Exception('HMAC does not match.');
  121. }
  122. $result = $this->cipher->decrypt($ciphertext);
  123. if ($result === false) {
  124. throw new \Exception('Decryption failed');
  125. }
  126. return $result;
  127. }
  128. }