* [ADD] New encryption schema by using Defuse/php-encryption library. WARNING: work in progress

This commit is contained in:
nuxsmin
2017-02-17 09:06:41 +01:00
parent 42af032550
commit fd25415e3b
44 changed files with 3455 additions and 383 deletions

View File

@@ -0,0 +1,434 @@
<?php
namespace Defuse\Crypto;
use Defuse\Crypto\Exception as Ex;
final class Core
{
const HEADER_VERSION_SIZE = 4;
const MINIMUM_CIPHERTEXT_SIZE = 84;
const CURRENT_VERSION = "\xDE\xF5\x02\x00";
const CIPHER_METHOD = 'aes-256-ctr';
const BLOCK_BYTE_SIZE = 16;
const KEY_BYTE_SIZE = 32;
const SALT_BYTE_SIZE = 32;
const MAC_BYTE_SIZE = 32;
const HASH_FUNCTION_NAME = 'sha256';
const ENCRYPTION_INFO_STRING = 'DefusePHP|V2|KeyForEncryption';
const AUTHENTICATION_INFO_STRING = 'DefusePHP|V2|KeyForAuthentication';
const BUFFER_BYTE_SIZE = 1048576;
const LEGACY_CIPHER_METHOD = 'aes-128-cbc';
const LEGACY_BLOCK_BYTE_SIZE = 16;
const LEGACY_KEY_BYTE_SIZE = 16;
const LEGACY_HASH_FUNCTION_NAME = 'sha256';
const LEGACY_MAC_BYTE_SIZE = 32;
const LEGACY_ENCRYPTION_INFO_STRING = 'DefusePHP|KeyForEncryption';
const LEGACY_AUTHENTICATION_INFO_STRING = 'DefusePHP|KeyForAuthentication';
/*
* V2.0 Format: VERSION (4 bytes) || SALT (32 bytes) || IV (16 bytes) ||
* CIPHERTEXT (varies) || HMAC (32 bytes)
*
* V1.0 Format: HMAC (32 bytes) || IV (16 bytes) || CIPHERTEXT (varies).
*/
/**
* Adds an integer to a block-sized counter.
*
* @param string $ctr
* @param int $inc
*
* @throws Ex\EnvironmentIsBrokenException
*
* @return string
*/
public static function incrementCounter($ctr, $inc)
{
if (Core::ourStrlen($ctr) !== Core::BLOCK_BYTE_SIZE) {
throw new Ex\EnvironmentIsBrokenException(
'Trying to increment a nonce of the wrong size.'
);
}
if (! \is_int($inc)) {
throw new Ex\EnvironmentIsBrokenException(
'Trying to increment nonce by a non-integer.'
);
}
if ($inc < 0) {
throw new Ex\EnvironmentIsBrokenException(
'Trying to increment nonce by a negative amount.'
);
}
if ($inc > PHP_INT_MAX - 255) {
throw new Ex\EnvironmentIsBrokenException(
'Integer overflow may occur.'
);
}
/*
* We start at the rightmost byte (big-endian)
* So, too, does OpenSSL: http://stackoverflow.com/a/3146214/2224584
*/
for ($i = Core::BLOCK_BYTE_SIZE - 1; $i >= 0; --$i) {
$sum = \ord($ctr[$i]) + $inc;
/* Detect integer overflow and fail. */
if (! \is_int($sum)) {
throw new Ex\EnvironmentIsBrokenException(
'Integer overflow in CTR mode nonce increment.'
);
}
$ctr[$i] = \pack('C', $sum & 0xFF);
$inc = $sum >> 8;
}
return $ctr;
}
/**
* Returns a random byte string of the specified length.
*
* @param int $octets
*
* @throws Ex\EnvironmentIsBrokenException
*
* @return string
*/
public static function secureRandom($octets)
{
self::ensureFunctionExists('random_bytes');
try {
return \random_bytes($octets);
} catch (\Exception $ex) {
throw new Ex\EnvironmentIsBrokenException(
'Your system does not have a secure random number generator.'
);
}
}
/**
* Computes the HKDF key derivation function specified in
* http://tools.ietf.org/html/rfc5869.
*
* @param string $hash Hash Function
* @param string $ikm Initial Keying Material
* @param int $length How many bytes?
* @param string $info What sort of key are we deriving?
* @param string $salt
*
* @throws Ex\EnvironmentIsBrokenException
*
* @return string
*/
public static function HKDF($hash, $ikm, $length, $info = '', $salt = null)
{
$digest_length = Core::ourStrlen(\hash_hmac($hash, '', '', true));
// Sanity-check the desired output length.
if (empty($length) || ! \is_int($length) ||
$length < 0 || $length > 255 * $digest_length) {
throw new Ex\EnvironmentIsBrokenException(
'Bad output length requested of HKDF.'
);
}
// "if [salt] not provided, is set to a string of HashLen zeroes."
if (\is_null($salt)) {
$salt = \str_repeat("\x00", $digest_length);
}
// HKDF-Extract:
// PRK = HMAC-Hash(salt, IKM)
// The salt is the HMAC key.
$prk = \hash_hmac($hash, $ikm, $salt, true);
// HKDF-Expand:
// This check is useless, but it serves as a reminder to the spec.
if (Core::ourStrlen($prk) < $digest_length) {
throw new Ex\EnvironmentIsBrokenException();
}
// T(0) = ''
$t = '';
$last_block = '';
for ($block_index = 1; Core::ourStrlen($t) < $length; ++$block_index) {
// T(i) = HMAC-Hash(PRK, T(i-1) | info | 0x??)
$last_block = \hash_hmac(
$hash,
$last_block . $info . \chr($block_index),
$prk,
true
);
// T = T(1) | T(2) | T(3) | ... | T(N)
$t .= $last_block;
}
// ORM = first L octets of T
$orm = Core::ourSubstr($t, 0, $length);
if ($orm === false) {
throw new Ex\EnvironmentIsBrokenException();
}
return $orm;
}
/**
* Checks if two equal-length strings are the same without leaking
* information through side channels.
*
* @param string $expected
* @param string $given
*
* @throws Ex\EnvironmentIsBrokenException
*
* @return bool
*/
public static function hashEquals($expected, $given)
{
static $native = null;
if ($native === null) {
$native = \function_exists('hash_equals');
}
if ($native) {
return \hash_equals($expected, $given);
}
// We can't just compare the strings with '==', since it would make
// timing attacks possible. We could use the XOR-OR constant-time
// comparison algorithm, but that may not be a reliable defense in an
// interpreted language. So we use the approach of HMACing both strings
// with a random key and comparing the HMACs.
// We're not attempting to make variable-length string comparison
// secure, as that's very difficult. Make sure the strings are the same
// length.
if (Core::ourStrlen($expected) !== Core::ourStrlen($given)) {
throw new Ex\EnvironmentIsBrokenException();
}
$blind = Core::secureRandom(32);
$message_compare = \hash_hmac(Core::HASH_FUNCTION_NAME, $given, $blind);
$correct_compare = \hash_hmac(Core::HASH_FUNCTION_NAME, $expected, $blind);
return $correct_compare === $message_compare;
}
/**
* Throws an exception if the constant doesn't exist.
*
* @param string $name
*
* @throws Ex\EnvironmentIsBrokenException
*/
public static function ensureConstantExists($name)
{
if (! \defined($name)) {
throw new Ex\EnvironmentIsBrokenException();
}
}
/**
* Throws an exception if the function doesn't exist.
*
* @param string $name
*
* @throws Ex\EnvironmentIsBrokenException
*/
public static function ensureFunctionExists($name)
{
if (! \function_exists($name)) {
throw new Ex\EnvironmentIsBrokenException();
}
}
/*
* We need these strlen() and substr() functions because when
* 'mbstring.func_overload' is set in php.ini, the standard strlen() and
* substr() are replaced by mb_strlen() and mb_substr().
*/
/**
* Computes the length of a string in bytes.
*
* @param string $str
*
* @throws Ex\EnvironmentIsBrokenException
*
* @return int
*/
public static function ourStrlen($str)
{
static $exists = null;
if ($exists === null) {
$exists = \function_exists('mb_strlen');
}
if ($exists) {
$length = \mb_strlen($str, '8bit');
if ($length === false) {
throw new Ex\EnvironmentIsBrokenException();
}
return $length;
} else {
return \strlen($str);
}
}
/**
* Behaves roughly like the function substr() in PHP 7 does.
*
* @param string $str
* @param int $start
* @param int $length
*
* @throws Ex\EnvironmentIsBrokenException
*
* @return string
*/
public static function ourSubstr($str, $start, $length = null)
{
static $exists = null;
if ($exists === null) {
$exists = \function_exists('mb_substr');
}
if ($exists) {
// mb_substr($str, 0, NULL, '8bit') returns an empty string on PHP
// 5.3, so we have to find the length ourselves.
if (! isset($length)) {
if ($start >= 0) {
$length = Core::ourStrlen($str) - $start;
} else {
$length = -$start;
}
}
// This is required to make mb_substr behavior identical to substr.
// Without this, mb_substr() would return false, contra to what the
// PHP documentation says (it doesn't say it can return false.)
if ($start === Core::ourStrlen($str) && $length === 0) {
return '';
}
if ($start > Core::ourStrlen($str)) {
return false;
}
$substr = \mb_substr($str, $start, $length, '8bit');
if (Core::ourStrlen($substr) !== $length) {
throw new Ex\EnvironmentIsBrokenException(
'Your version of PHP has bug #66797. Its implementation of
mb_substr() is incorrect. See the details here:
https://bugs.php.net/bug.php?id=66797'
);
}
return $substr;
}
// Unlike mb_substr(), substr() doesn't accept NULL for length
if (isset($length)) {
return \substr($str, $start, $length);
} else {
return \substr($str, $start);
}
}
/**
* Computes the PBKDF2 password-based key derivation function.
*
* The PBKDF2 function is defined in RFC 2898. Test vectors can be found in
* RFC 6070. This implementation of PBKDF2 was originally created by Taylor
* Hornby, with improvements from http://www.variations-of-shadow.com/.
*
* @param string $algorithm The hash algorithm to use. Recommended: SHA256
* @param string $password The password.
* @param string $salt A salt that is unique to the password.
* @param int $count Iteration count. Higher is better, but slower. Recommended: At least 1000.
* @param int $key_length The length of the derived key in bytes.
* @param bool $raw_output If true, the key is returned in raw binary format. Hex encoded otherwise.
*
* @throws Ex\EnvironmentIsBrokenException
*
* @return string A $key_length-byte key derived from the password and salt.
*/
public static function pbkdf2($algorithm, $password, $salt, $count, $key_length, $raw_output = false)
{
// Type checks:
if (! \is_string($algorithm)) {
throw new \InvalidArgumentException(
'pbkdf2(): algorithm must be a string'
);
}
if (! \is_string($password)) {
throw new \InvalidArgumentException(
'pbkdf2(): password must be a string'
);
}
if (! \is_string($salt)) {
throw new \InvalidArgumentException(
'pbkdf2(): salt must be a string'
);
}
// Coerce strings to integers with no information loss or overflow
$count += 0;
$key_length += 0;
$algorithm = \strtolower($algorithm);
if (! \in_array($algorithm, \hash_algos(), true)) {
throw new Ex\EnvironmentIsBrokenException(
'Invalid or unsupported hash algorithm.'
);
}
// Whitelist, or we could end up with people using CRC32.
$ok_algorithms = [
'sha1', 'sha224', 'sha256', 'sha384', 'sha512',
'ripemd160', 'ripemd256', 'ripemd320', 'whirlpool',
];
if (! \in_array($algorithm, $ok_algorithms, true)) {
throw new Ex\EnvironmentIsBrokenException(
'Algorithm is not a secure cryptographic hash function.'
);
}
if ($count <= 0 || $key_length <= 0) {
throw new Ex\EnvironmentIsBrokenException(
'Invalid PBKDF2 parameters.'
);
}
if (\function_exists('hash_pbkdf2')) {
// The output length is in NIBBLES (4-bits) if $raw_output is false!
if (! $raw_output) {
$key_length = $key_length * 2;
}
return \hash_pbkdf2($algorithm, $password, $salt, $count, $key_length, $raw_output);
}
$hash_length = Core::ourStrlen(\hash($algorithm, '', true));
$block_count = \ceil($key_length / $hash_length);
$output = '';
for ($i = 1; $i <= $block_count; $i++) {
// $i encoded as 4 bytes, big endian.
$last = $salt . \pack('N', $i);
// first iteration
$last = $xorsum = \hash_hmac($algorithm, $last, $password, true);
// perform the other $count - 1 iterations
for ($j = 1; $j < $count; $j++) {
$xorsum ^= ($last = \hash_hmac($algorithm, $last, $password, true));
}
$output .= $xorsum;
}
if ($raw_output) {
return Core::ourSubstr($output, 0, $key_length);
} else {
return Encoding::binToHex(Core::ourSubstr($output, 0, $key_length));
}
}
}

View File

@@ -0,0 +1,372 @@
<?php
namespace Defuse\Crypto;
use Defuse\Crypto\Exception as Ex;
class Crypto
{
/**
* Encrypts a string with a Key.
*
* @param string $plaintext
* @param Key $key
* @param bool $raw_binary
*
* @throws Ex\EnvironmentIsBrokenException
*
* @return string
*/
public static function encrypt($plaintext, Key $key, $raw_binary = false)
{
return self::encryptInternal(
$plaintext,
KeyOrPassword::createFromKey($key),
$raw_binary
);
}
/**
* Encrypts a string with a password, using a slow key derivation function
* to make password cracking more expensive.
*
* @param string $plaintext
* @param string $password
* @param bool $raw_binary
*
* @throws Ex\EnvironmentIsBrokenException
*
* @return string
*/
public static function encryptWithPassword($plaintext, $password, $raw_binary = false)
{
return self::encryptInternal(
$plaintext,
KeyOrPassword::createFromPassword($password),
$raw_binary
);
}
/**
* Decrypts a ciphertext to a string with a Key.
*
* @param string $ciphertext
* @param Key $key
* @param bool $raw_binary
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\WrongKeyOrModifiedCiphertextException
*
* @return string
*/
public static function decrypt($ciphertext, Key $key, $raw_binary = false)
{
return self::decryptInternal(
$ciphertext,
KeyOrPassword::createFromKey($key),
$raw_binary
);
}
/**
* Decrypts a ciphertext to a string with a password, using a slow key
* derivation function to make password cracking more expensive.
*
* @param string $ciphertext
* @param string $password
* @param bool $raw_binary
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\WrongKeyOrModifiedCiphertextException
*
* @return string
*/
public static function decryptWithPassword($ciphertext, $password, $raw_binary = false)
{
return self::decryptInternal(
$ciphertext,
KeyOrPassword::createFromPassword($password),
$raw_binary
);
}
/**
* Decrypts a legacy ciphertext produced by version 1 of this library.
*
* @param string $ciphertext
* @param string $key
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\WrongKeyOrModifiedCiphertextException
*
* @return string
*/
public static function legacyDecrypt($ciphertext, $key)
{
RuntimeTests::runtimeTest();
// Extract the HMAC from the front of the ciphertext.
if (Core::ourStrlen($ciphertext) <= Core::LEGACY_MAC_BYTE_SIZE) {
throw new Ex\WrongKeyOrModifiedCiphertextException(
'Ciphertext is too short.'
);
}
$hmac = Core::ourSubstr($ciphertext, 0, Core::LEGACY_MAC_BYTE_SIZE);
if ($hmac === false) {
throw new Ex\EnvironmentIsBrokenException();
}
$ciphertext = Core::ourSubstr($ciphertext, Core::LEGACY_MAC_BYTE_SIZE);
if ($ciphertext === false) {
throw new Ex\EnvironmentIsBrokenException();
}
// Regenerate the same authentication sub-key.
$akey = Core::HKDF(
Core::LEGACY_HASH_FUNCTION_NAME,
$key,
Core::LEGACY_KEY_BYTE_SIZE,
Core::LEGACY_AUTHENTICATION_INFO_STRING,
null
);
if (self::verifyHMAC($hmac, $ciphertext, $akey)) {
// Regenerate the same encryption sub-key.
$ekey = Core::HKDF(
Core::LEGACY_HASH_FUNCTION_NAME,
$key,
Core::LEGACY_KEY_BYTE_SIZE,
Core::LEGACY_ENCRYPTION_INFO_STRING,
null
);
// Extract the IV from the ciphertext.
if (Core::ourStrlen($ciphertext) <= Core::LEGACY_BLOCK_BYTE_SIZE) {
throw new Ex\WrongKeyOrModifiedCiphertextException(
'Ciphertext is too short.'
);
}
$iv = Core::ourSubstr($ciphertext, 0, Core::LEGACY_BLOCK_BYTE_SIZE);
if ($iv === false) {
throw new Ex\EnvironmentIsBrokenException();
}
$ciphertext = Core::ourSubstr($ciphertext, Core::LEGACY_BLOCK_BYTE_SIZE);
if ($ciphertext === false) {
throw new Ex\EnvironmentIsBrokenException();
}
// Do the decryption.
$plaintext = self::plainDecrypt($ciphertext, $ekey, $iv, Core::LEGACY_CIPHER_METHOD);
return $plaintext;
} else {
throw new Ex\WrongKeyOrModifiedCiphertextException(
'Integrity check failed.'
);
}
}
/**
* Encrypts a string with either a key or a password.
*
* @param string $plaintext
* @param KeyOrPassword $secret
* @param bool $raw_binary
*
* @return string
*/
private static function encryptInternal($plaintext, KeyOrPassword $secret, $raw_binary)
{
RuntimeTests::runtimeTest();
$salt = Core::secureRandom(Core::SALT_BYTE_SIZE);
$keys = $secret->deriveKeys($salt);
$ekey = $keys->getEncryptionKey();
$akey = $keys->getAuthenticationKey();
$iv = Core::secureRandom(Core::BLOCK_BYTE_SIZE);
$ciphertext = Core::CURRENT_VERSION . $salt . $iv . self::plainEncrypt($plaintext, $ekey, $iv);
$auth = \hash_hmac(Core::HASH_FUNCTION_NAME, $ciphertext, $akey, true);
$ciphertext = $ciphertext . $auth;
if ($raw_binary) {
return $ciphertext;
}
return Encoding::binToHex($ciphertext);
}
/**
* Decrypts a ciphertext to a string with either a key or a password.
*
* @param string $ciphertext
* @param KeyOrPassword $secret
* @param bool $raw_binary
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\WrongKeyOrModifiedCiphertextException
*
* @return string
*/
private static function decryptInternal($ciphertext, KeyOrPassword $secret, $raw_binary)
{
RuntimeTests::runtimeTest();
if (! $raw_binary) {
try {
$ciphertext = Encoding::hexToBin($ciphertext);
} catch (Ex\BadFormatException $ex) {
throw new Ex\WrongKeyOrModifiedCiphertextException(
'Ciphertext has invalid hex encoding.'
);
}
}
if (Core::ourStrlen($ciphertext) < Core::MINIMUM_CIPHERTEXT_SIZE) {
throw new Ex\WrongKeyOrModifiedCiphertextException(
'Ciphertext is too short.'
);
}
// Get and check the version header.
$header = Core::ourSubstr($ciphertext, 0, Core::HEADER_VERSION_SIZE);
if ($header !== Core::CURRENT_VERSION) {
throw new Ex\WrongKeyOrModifiedCiphertextException(
'Bad version header.'
);
}
// Get the salt.
$salt = Core::ourSubstr(
$ciphertext,
Core::HEADER_VERSION_SIZE,
Core::SALT_BYTE_SIZE
);
if ($salt === false) {
throw new Ex\EnvironmentIsBrokenException();
}
// Get the IV.
$iv = Core::ourSubstr(
$ciphertext,
Core::HEADER_VERSION_SIZE + Core::SALT_BYTE_SIZE,
Core::BLOCK_BYTE_SIZE
);
if ($iv === false) {
throw new Ex\EnvironmentIsBrokenException();
}
// Get the HMAC.
$hmac = Core::ourSubstr(
$ciphertext,
Core::ourStrlen($ciphertext) - Core::MAC_BYTE_SIZE,
Core::MAC_BYTE_SIZE
);
if ($hmac === false) {
throw new Ex\EnvironmentIsBrokenException();
}
// Get the actual encrypted ciphertext.
$encrypted = Core::ourSubstr(
$ciphertext,
Core::HEADER_VERSION_SIZE + Core::SALT_BYTE_SIZE +
Core::BLOCK_BYTE_SIZE,
Core::ourStrlen($ciphertext) - Core::MAC_BYTE_SIZE - Core::SALT_BYTE_SIZE -
Core::BLOCK_BYTE_SIZE - Core::HEADER_VERSION_SIZE
);
if ($encrypted === false) {
throw new Ex\EnvironmentIsBrokenException();
}
// Derive the separate encryption and authentication keys from the key
// or password, whichever it is.
$keys = $secret->deriveKeys($salt);
if (self::verifyHMAC($hmac, $header . $salt . $iv . $encrypted, $keys->getAuthenticationKey())) {
$plaintext = self::plainDecrypt($encrypted, $keys->getEncryptionKey(), $iv, Core::CIPHER_METHOD);
return $plaintext;
} else {
throw new Ex\WrongKeyOrModifiedCiphertextException(
'Integrity check failed.'
);
}
}
/**
* Raw unauthenticated encryption (insecure on its own).
*
* @param string $plaintext
* @param string $key
* @param string $iv
*
* @throws Ex\EnvironmentIsBrokenException
*
* @return string
*/
protected static function plainEncrypt($plaintext, $key, $iv)
{
Core::ensureConstantExists('OPENSSL_RAW_DATA');
Core::ensureFunctionExists('openssl_encrypt');
$ciphertext = \openssl_encrypt(
$plaintext,
Core::CIPHER_METHOD,
$key,
OPENSSL_RAW_DATA,
$iv
);
if ($ciphertext === false) {
throw new Ex\EnvironmentIsBrokenException(
'openssl_encrypt() failed.'
);
}
return $ciphertext;
}
/**
* Raw unauthenticated decryption (insecure on its own).
*
* @param string $ciphertext
* @param string $key
* @param string $iv
* @param string $cipherMethod
*
* @throws Ex\EnvironmentIsBrokenException
*
* @return string
*/
protected static function plainDecrypt($ciphertext, $key, $iv, $cipherMethod)
{
Core::ensureConstantExists('OPENSSL_RAW_DATA');
Core::ensureFunctionExists('openssl_decrypt');
$plaintext = \openssl_decrypt(
$ciphertext,
$cipherMethod,
$key,
OPENSSL_RAW_DATA,
$iv
);
if ($plaintext === false) {
throw new Ex\EnvironmentIsBrokenException(
'openssl_decrypt() failed.'
);
}
return $plaintext;
}
/**
* Verifies an HMAC without leaking information through side-channels.
*
* @param string $correct_hmac
* @param string $message
* @param string $key
*
* @throws Ex\EnvironmentIsBrokenException
*
* @return bool
*/
protected static function verifyHMAC($correct_hmac, $message, $key)
{
$message_hmac = \hash_hmac(Core::HASH_FUNCTION_NAME, $message, $key, true);
return Core::hashEquals($correct_hmac, $message_hmac);
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Defuse\Crypto;
final class DerivedKeys
{
private $akey = null;
private $ekey = null;
/**
* Returns the authentication key.
*/
public function getAuthenticationKey()
{
return $this->akey;
}
/**
* Returns the encryption key.
*/
public function getEncryptionKey()
{
return $this->ekey;
}
/**
* Constructor for DerivedKeys.
*
* @param string $akey
* @param string $ekey
*/
public function __construct($akey, $ekey)
{
$this->akey = $akey;
$this->ekey = $ekey;
}
}

View File

@@ -0,0 +1,212 @@
<?php
namespace Defuse\Crypto;
use Defuse\Crypto\Exception as Ex;
final class Encoding
{
const CHECKSUM_BYTE_SIZE = 32;
const CHECKSUM_HASH_ALGO = 'sha256';
const SERIALIZE_HEADER_BYTES = 4;
/**
* Converts a byte string to a hexadecimal string without leaking
* information through side channels.
*
* @param string $byte_string
*
* @throws Ex\EnvironmentIsBrokenException
*
* @return string
*/
public static function binToHex($byte_string)
{
$hex = '';
$len = Core::ourStrlen($byte_string);
for ($i = 0; $i < $len; ++$i) {
$c = \ord($byte_string[$i]) & 0xf;
$b = \ord($byte_string[$i]) >> 4;
$hex .= \pack(
'CC',
87 + $b + ((($b - 10) >> 8) & ~38),
87 + $c + ((($c - 10) >> 8) & ~38)
);
}
return $hex;
}
/**
* Converts a hexadecimal string into a byte string without leaking
* information through side channels.
*
* @param string $hex_string
*
* @throws Ex\BadFormatException
* @throws Ex\EnvironmentIsBrokenException
*
* @return string
*/
public static function hexToBin($hex_string)
{
$hex_pos = 0;
$bin = '';
$hex_len = Core::ourStrlen($hex_string);
$state = 0;
$c_acc = 0;
while ($hex_pos < $hex_len) {
$c = \ord($hex_string[$hex_pos]);
$c_num = $c ^ 48;
$c_num0 = ($c_num - 10) >> 8;
$c_alpha = ($c & ~32) - 55;
$c_alpha0 = (($c_alpha - 10) ^ ($c_alpha - 16)) >> 8;
if (($c_num0 | $c_alpha0) === 0) {
throw new Ex\BadFormatException(
'Encoding::hexToBin() input is not a hex string.'
);
}
$c_val = ($c_num0 & $c_num) | ($c_alpha & $c_alpha0);
if ($state === 0) {
$c_acc = $c_val * 16;
} else {
$bin .= \pack('C', $c_acc | $c_val);
}
$state ^= 1;
++$hex_pos;
}
return $bin;
}
/*
* SECURITY NOTE ON APPLYING CHECKSUMS TO SECRETS:
*
* The checksum introduces a potential security weakness. For example,
* suppose we apply a checksum to a key, and that an adversary has an
* exploit against the process containing the key, such that they can
* overwrite an arbitrary byte of memory and then cause the checksum to
* be verified and learn the result.
*
* In this scenario, the adversary can extract the key one byte at
* a time by overwriting it with their guess of its value and then
* asking if the checksum matches. If it does, their guess was right.
* This kind of attack may be more easy to implement and more reliable
* than a remote code execution attack.
*
* This attack also applies to authenticated encryption as a whole, in
* the situation where the adversary can overwrite a byte of the key
* and then cause a valid ciphertext to be decrypted, and then
* determine whether the MAC check passed or failed.
*
* By using the full SHA256 hash instead of truncating it, I'm ensuring
* that both ways of going about the attack are equivalently difficult.
* A shorter checksum of say 32 bits might be more useful to the
* adversary as an oracle in case their writes are coarser grained.
*
* Because the scenario assumes a serious vulnerability, we don't try
* to prevent attacks of this style.
*/
/**
* INTERNAL USE ONLY: Applies a version header, applies a checksum, and
* then encodes a byte string into a range of printable ASCII characters.
*
* @param string $header
* @param string $bytes
*
* @throws Ex\EnvironmentIsBrokenException
*
* @return string
*/
public static function saveBytesToChecksummedAsciiSafeString($header, $bytes)
{
// Headers must be a constant length to prevent one type's header from
// being a prefix of another type's header, leading to ambiguity.
if (Core::ourStrlen($header) !== self::SERIALIZE_HEADER_BYTES) {
throw new Ex\EnvironmentIsBrokenException(
'Header must be ' . self::SERIALIZE_HEADER_BYTES . ' bytes.'
);
}
return Encoding::binToHex(
$header .
$bytes .
\hash(
self::CHECKSUM_HASH_ALGO,
$header . $bytes,
true
)
);
}
/**
* INTERNAL USE ONLY: Decodes, verifies the header and checksum, and returns
* the encoded byte string.
*
* @param string $expected_header
* @param string $string
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\BadFormatException
*
* @return string
*/
public static function loadBytesFromChecksummedAsciiSafeString($expected_header, $string)
{
// Headers must be a constant length to prevent one type's header from
// being a prefix of another type's header, leading to ambiguity.
if (Core::ourStrlen($expected_header) !== self::SERIALIZE_HEADER_BYTES) {
throw new Ex\EnvironmentIsBrokenException(
'Header must be 4 bytes.'
);
}
$bytes = Encoding::hexToBin($string);
/* Make sure we have enough bytes to get the version header and checksum. */
if (Core::ourStrlen($bytes) < self::SERIALIZE_HEADER_BYTES + self::CHECKSUM_BYTE_SIZE) {
throw new Ex\BadFormatException(
'Encoded data is shorter than expected.'
);
}
/* Grab the version header. */
$actual_header = Core::ourSubstr($bytes, 0, self::SERIALIZE_HEADER_BYTES);
if ($actual_header !== $expected_header) {
throw new Ex\BadFormatException(
'Invalid header.'
);
}
/* Grab the bytes that are part of the checksum. */
$checked_bytes = Core::ourSubstr(
$bytes,
0,
Core::ourStrlen($bytes) - self::CHECKSUM_BYTE_SIZE
);
/* Grab the included checksum. */
$checksum_a = Core::ourSubstr(
$bytes,
Core::ourStrlen($bytes) - self::CHECKSUM_BYTE_SIZE,
self::CHECKSUM_BYTE_SIZE
);
/* Re-compute the checksum. */
$checksum_b = \hash(self::CHECKSUM_HASH_ALGO, $checked_bytes, true);
/* Check if the checksum matches. */
if (! Core::hashEquals($checksum_a, $checksum_b)) {
throw new Ex\BadFormatException(
"Data is corrupted, the checksum doesn't match"
);
}
return Core::ourSubstr(
$bytes,
self::SERIALIZE_HEADER_BYTES,
Core::ourStrlen($bytes) - self::SERIALIZE_HEADER_BYTES - self::CHECKSUM_BYTE_SIZE
);
}
}

View File

@@ -0,0 +1,7 @@
<?php
namespace Defuse\Crypto\Exception;
class BadFormatException extends \Defuse\Crypto\Exception\CryptoException
{
}

View File

@@ -0,0 +1,7 @@
<?php
namespace Defuse\Crypto\Exception;
class CryptoException extends \Exception
{
}

View File

@@ -0,0 +1,7 @@
<?php
namespace Defuse\Crypto\Exception;
class EnvironmentIsBrokenException extends \Defuse\Crypto\Exception\CryptoException
{
}

View File

@@ -0,0 +1,7 @@
<?php
namespace Defuse\Crypto\Exception;
class IOException extends \Defuse\Crypto\Exception\CryptoException
{
}

View File

@@ -0,0 +1,7 @@
<?php
namespace Defuse\Crypto\Exception;
class WrongKeyOrModifiedCiphertextException extends \Defuse\Crypto\Exception\CryptoException
{
}

View File

@@ -0,0 +1,755 @@
<?php
namespace Defuse\Crypto;
use Defuse\Crypto\Exception as Ex;
final class File
{
/**
* Encrypts the input file, saving the ciphertext to the output file.
*
* @param string $inputFilename
* @param string $outputFilename
* @param Key $key
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\IOException
*/
public static function encryptFile($inputFilename, $outputFilename, Key $key)
{
self::encryptFileInternal(
$inputFilename,
$outputFilename,
KeyOrPassword::createFromKey($key)
);
}
/**
* Encrypts a file with a password, using a slow key derivation function to
* make password cracking more expensive.
*
* @param string $inputFilename
* @param string $outputFilename
* @param string $password
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\IOException
*/
public static function encryptFileWithPassword($inputFilename, $outputFilename, $password)
{
self::encryptFileInternal(
$inputFilename,
$outputFilename,
KeyOrPassword::createFromPassword($password)
);
}
/**
* Decrypts the input file, saving the plaintext to the output file.
*
* @param string $inputFilename
* @param string $outputFilename
* @param Key $key
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\IOException
* @throws Ex\WrongKeyOrModifiedCiphertextException
*/
public static function decryptFile($inputFilename, $outputFilename, Key $key)
{
self::decryptFileInternal(
$inputFilename,
$outputFilename,
KeyOrPassword::createFromKey($key)
);
}
/**
* Decrypts a file with a password, using a slow key derivation function to
* make password cracking more expensive.
*
* @param string $inputFilename
* @param string $outputFilename
* @param string $password
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\IOException
* @throws Ex\WrongKeyOrModifiedCiphertextException
*/
public static function decryptFileWithPassword($inputFilename, $outputFilename, $password)
{
self::decryptFileInternal(
$inputFilename,
$outputFilename,
KeyOrPassword::createFromPassword($password)
);
}
/**
* Takes two resource handles and encrypts the contents of the first,
* writing the ciphertext into the second.
*
* @param resource $inputHandle
* @param resource $outputHandle
* @param Key $key
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\WrongKeyOrModifiedCiphertextException
*/
public static function encryptResource($inputHandle, $outputHandle, Key $key)
{
self::encryptResourceInternal(
$inputHandle,
$outputHandle,
KeyOrPassword::createFromKey($key)
);
}
/**
* Encrypts the contents of one resource handle into another with a
* password, using a slow key derivation function to make password cracking
* more expensive.
*
* @param resource $inputHandle
* @param resource $outputHandle
* @param string $password
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\IOException
* @throws Ex\WrongKeyOrModifiedCiphertextException
*/
public static function encryptResourceWithPassword($inputHandle, $outputHandle, $password)
{
self::encryptResourceInternal(
$inputHandle,
$outputHandle,
KeyOrPassword::createFromPassword($password)
);
}
/**
* Takes two resource handles and decrypts the contents of the first,
* writing the plaintext into the second.
*
* @param resource $inputHandle
* @param resource $outputHandle
* @param Key $key
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\IOException
* @throws Ex\WrongKeyOrModifiedCiphertextException
*/
public static function decryptResource($inputHandle, $outputHandle, Key $key)
{
self::decryptResourceInternal(
$inputHandle,
$outputHandle,
KeyOrPassword::createFromKey($key)
);
}
/**
* Decrypts the contents of one resource into another with a password, using
* a slow key derivation function to make password cracking more expensive.
*
* @param resource $inputHandle
* @param resource $outputHandle
* @param string $password
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\IOException
* @throws Ex\WrongKeyOrModifiedCiphertextException
*/
public static function decryptResourceWithPassword($inputHandle, $outputHandle, $password)
{
self::decryptResourceInternal(
$inputHandle,
$outputHandle,
KeyOrPassword::createFromPassword($password)
);
}
/**
* Encrypts a file with either a key or a password.
*
* @param string $inputFilename
* @param string $outputFilename
* @param KeyOrPassword $secret
*
* @throws Ex\CryptoException
* @throws Ex\IOException
*/
private static function encryptFileInternal($inputFilename, $outputFilename, KeyOrPassword $secret)
{
/* Open the input file. */
$if = @\fopen($inputFilename, 'rb');
if ($if === false) {
throw new Ex\IOException(
'Cannot open input file for encrypting: ' .
self::getLastErrorMessage()
);
}
if (\is_callable('\\stream_set_read_buffer')) {
/* This call can fail, but the only consequence is performance. */
\stream_set_read_buffer($if, 0);
}
/* Open the output file. */
$of = @\fopen($outputFilename, 'wb');
if ($of === false) {
\fclose($if);
throw new Ex\IOException(
'Cannot open output file for encrypting: ' .
self::getLastErrorMessage()
);
}
if (\is_callable('\\stream_set_write_buffer')) {
/* This call can fail, but the only consequence is performance. */
\stream_set_write_buffer($of, 0);
}
/* Perform the encryption. */
try {
self::encryptResourceInternal($if, $of, $secret);
} catch (Ex\CryptoException $ex) {
\fclose($if);
\fclose($of);
throw $ex;
}
/* Close the input file. */
if (\fclose($if) === false) {
\fclose($of);
throw new Ex\IOException(
'Cannot close input file after encrypting'
);
}
/* Close the output file. */
if (\fclose($of) === false) {
throw new Ex\IOException(
'Cannot close output file after encrypting'
);
}
}
/**
* Decrypts a file with either a key or a password.
*
* @param string $inputFilename
* @param string $outputFilename
* @param KeyOrPassword $secret
*
* @throws Ex\CryptoException
* @throws Ex\IOException
*/
private static function decryptFileInternal($inputFilename, $outputFilename, KeyOrPassword $secret)
{
/* Open the input file. */
$if = @\fopen($inputFilename, 'rb');
if ($if === false) {
throw new Ex\IOException(
'Cannot open input file for decrypting: ' .
self::getLastErrorMessage()
);
}
if (\is_callable('\\stream_set_read_buffer')) {
/* This call can fail, but the only consequence is performance. */
\stream_set_read_buffer($if, 0);
}
/* Open the output file. */
$of = @\fopen($outputFilename, 'wb');
if ($of === false) {
\fclose($if);
throw new Ex\IOException(
'Cannot open output file for decrypting: ' .
self::getLastErrorMessage()
);
}
if (\is_callable('\\stream_set_write_buffer')) {
/* This call can fail, but the only consequence is performance. */
\stream_set_write_buffer($of, 0);
}
/* Perform the decryption. */
try {
self::decryptResourceInternal($if, $of, $secret);
} catch (Ex\CryptoException $ex) {
\fclose($if);
\fclose($of);
throw $ex;
}
/* Close the input file. */
if (\fclose($if) === false) {
\fclose($of);
throw new Ex\IOException(
'Cannot close input file after decrypting'
);
}
/* Close the output file. */
if (\fclose($of) === false) {
throw new Ex\IOException(
'Cannot close output file after decrypting'
);
}
}
/**
* Encrypts a resource with either a key or a password.
*
* @param resource $inputHandle
* @param resource $outputHandle
* @param KeyOrPassword $secret
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\IOException
*/
private static function encryptResourceInternal($inputHandle, $outputHandle, KeyOrPassword $secret)
{
if (! \is_resource($inputHandle)) {
throw new Ex\IOException(
'Input handle must be a resource!'
);
}
if (! \is_resource($outputHandle)) {
throw new Ex\IOException(
'Output handle must be a resource!'
);
}
$inputStat = \fstat($inputHandle);
$inputSize = $inputStat['size'];
$file_salt = Core::secureRandom(Core::SALT_BYTE_SIZE);
$keys = $secret->deriveKeys($file_salt);
$ekey = $keys->getEncryptionKey();
$akey = $keys->getAuthenticationKey();
$ivsize = Core::BLOCK_BYTE_SIZE;
$iv = Core::secureRandom($ivsize);
/* Initialize a streaming HMAC state. */
$hmac = \hash_init(Core::HASH_FUNCTION_NAME, HASH_HMAC, $akey);
if ($hmac === false) {
throw new Ex\EnvironmentIsBrokenException(
'Cannot initialize a hash context'
);
}
/* Write the header, salt, and IV. */
self::writeBytes(
$outputHandle,
Core::CURRENT_VERSION . $file_salt . $iv,
Core::HEADER_VERSION_SIZE + Core::SALT_BYTE_SIZE + $ivsize
);
/* Add the header, salt, and IV to the HMAC. */
\hash_update($hmac, Core::CURRENT_VERSION);
\hash_update($hmac, $file_salt);
\hash_update($hmac, $iv);
/* $thisIv will be incremented after each call to the encryption. */
$thisIv = $iv;
/* How many blocks do we encrypt at a time? We increment by this value. */
$inc = Core::BUFFER_BYTE_SIZE / Core::BLOCK_BYTE_SIZE;
/* Loop until we reach the end of the input file. */
$at_file_end = false;
while (! (\feof($inputHandle) || $at_file_end)) {
/* Find out if we can read a full buffer, or only a partial one. */
$pos = \ftell($inputHandle);
if ($pos === false) {
throw new Ex\IOException(
'Could not get current position in input file during encryption'
);
}
if ($pos + Core::BUFFER_BYTE_SIZE >= $inputSize) {
/* We're at the end of the file, so we need to break out of the loop. */
$at_file_end = true;
$read = self::readBytes(
$inputHandle,
$inputSize - $pos
);
} else {
$read = self::readBytes(
$inputHandle,
Core::BUFFER_BYTE_SIZE
);
}
/* Encrypt this buffer. */
$encrypted = \openssl_encrypt(
$read,
Core::CIPHER_METHOD,
$ekey,
OPENSSL_RAW_DATA,
$thisIv
);
if ($encrypted === false) {
throw new Ex\EnvironmentIsBrokenException(
'OpenSSL encryption error'
);
}
/* Write this buffer's ciphertext. */
self::writeBytes($outputHandle, $encrypted, Core::ourStrlen($encrypted));
/* Add this buffer's ciphertext to the HMAC. */
\hash_update($hmac, $encrypted);
/* Increment the counter by the number of blocks in a buffer. */
$thisIv = Core::incrementCounter($thisIv, $inc);
/* WARNING: Usually, unless the file is a multiple of the buffer
* size, $thisIv will contain an incorrect value here on the last
* iteration of this loop. */
}
/* Get the HMAC and append it to the ciphertext. */
$final_mac = \hash_final($hmac, true);
self::writeBytes($outputHandle, $final_mac, Core::MAC_BYTE_SIZE);
}
/**
* Decrypts a file-backed resource with either a key or a password.
*
* @param resource $inputHandle
* @param resource $outputHandle
* @param KeyOrPassword $secret
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\IOException
* @throws Ex\WrongKeyOrModifiedCiphertextException
*/
public static function decryptResourceInternal($inputHandle, $outputHandle, KeyOrPassword $secret)
{
if (! \is_resource($inputHandle)) {
throw new Ex\IOException(
'Input handle must be a resource!'
);
}
if (! \is_resource($outputHandle)) {
throw new Ex\IOException(
'Output handle must be a resource!'
);
}
/* Make sure the file is big enough for all the reads we need to do. */
$stat = \fstat($inputHandle);
if ($stat['size'] < Core::MINIMUM_CIPHERTEXT_SIZE) {
throw new Ex\WrongKeyOrModifiedCiphertextException(
'Input file is too small to have been created by this library.'
);
}
/* Check the version header. */
$header = self::readBytes($inputHandle, Core::HEADER_VERSION_SIZE);
if ($header !== Core::CURRENT_VERSION) {
throw new Ex\WrongKeyOrModifiedCiphertextException(
'Bad version header.'
);
}
/* Get the salt. */
$file_salt = self::readBytes($inputHandle, Core::SALT_BYTE_SIZE);
/* Get the IV. */
$ivsize = Core::BLOCK_BYTE_SIZE;
$iv = self::readBytes($inputHandle, $ivsize);
/* Derive the authentication and encryption keys. */
$keys = $secret->deriveKeys($file_salt);
$ekey = $keys->getEncryptionKey();
$akey = $keys->getAuthenticationKey();
/* We'll store the MAC of each buffer-sized chunk as we verify the
* actual MAC, so that we can check them again when decrypting. */
$macs = [];
/* $thisIv will be incremented after each call to the decryption. */
$thisIv = $iv;
/* How many blocks do we encrypt at a time? We increment by this value. */
$inc = Core::BUFFER_BYTE_SIZE / Core::BLOCK_BYTE_SIZE;
/* Get the HMAC. */
if (\fseek($inputHandle, (-1 * Core::MAC_BYTE_SIZE), SEEK_END) === false) {
throw new Ex\IOException(
'Cannot seek to beginning of MAC within input file'
);
}
/* Get the position of the last byte in the actual ciphertext. */
$cipher_end = \ftell($inputHandle);
if ($cipher_end === false) {
throw new Ex\IOException(
'Cannot read input file'
);
}
/* We have the position of the first byte of the HMAC. Go back by one. */
--$cipher_end;
/* Read the HMAC. */
$stored_mac = self::readBytes($inputHandle, Core::MAC_BYTE_SIZE);
/* Initialize a streaming HMAC state. */
$hmac = \hash_init(Core::HASH_FUNCTION_NAME, HASH_HMAC, $akey);
if ($hmac === false) {
throw new Ex\EnvironmentIsBrokenException(
'Cannot initialize a hash context'
);
}
/* Reset file pointer to the beginning of the file after the header */
if (\fseek($inputHandle, Core::HEADER_VERSION_SIZE, SEEK_SET) === false) {
throw new Ex\IOException(
'Cannot read seek within input file'
);
}
/* Seek to the start of the actual ciphertext. */
if (\fseek($inputHandle, Core::SALT_BYTE_SIZE + $ivsize, SEEK_CUR) === false) {
throw new Ex\IOException(
'Cannot seek input file to beginning of ciphertext'
);
}
/* PASS #1: Calculating the HMAC. */
\hash_update($hmac, $header);
\hash_update($hmac, $file_salt);
\hash_update($hmac, $iv);
$hmac2 = \hash_copy($hmac);
$break = false;
while (! $break) {
$pos = \ftell($inputHandle);
if ($pos === false) {
throw new Ex\IOException(
'Could not get current position in input file during decryption'
);
}
/* Read the next buffer-sized chunk (or less). */
if ($pos + Core::BUFFER_BYTE_SIZE >= $cipher_end) {
$break = true;
$read = self::readBytes(
$inputHandle,
$cipher_end - $pos + 1
);
} else {
$read = self::readBytes(
$inputHandle,
Core::BUFFER_BYTE_SIZE
);
}
/* Update the HMAC. */
\hash_update($hmac, $read);
/* Remember this buffer-sized chunk's HMAC. */
$chunk_mac = \hash_copy($hmac);
if ($chunk_mac === false) {
throw new Ex\EnvironmentIsBrokenException(
'Cannot duplicate a hash context'
);
}
$macs []= \hash_final($chunk_mac);
}
/* Get the final HMAC, which should match the stored one. */
$final_mac = \hash_final($hmac, true);
/* Verify the HMAC. */
if (! Core::hashEquals($final_mac, $stored_mac)) {
throw new Ex\WrongKeyOrModifiedCiphertextException(
'Integrity check failed.'
);
}
/* PASS #2: Decrypt and write output. */
/* Rewind to the start of the actual ciphertext. */
if (\fseek($inputHandle, Core::SALT_BYTE_SIZE + $ivsize + Core::HEADER_VERSION_SIZE, SEEK_SET) === false) {
throw new Ex\IOException(
'Could not move the input file pointer during decryption'
);
}
$at_file_end = false;
while (! $at_file_end) {
$pos = \ftell($inputHandle);
if ($pos === false) {
throw new Ex\IOException(
'Could not get current position in input file during decryption'
);
}
/* Read the next buffer-sized chunk (or less). */
if ($pos + Core::BUFFER_BYTE_SIZE >= $cipher_end) {
$at_file_end = true;
$read = self::readBytes(
$inputHandle,
$cipher_end - $pos + 1
);
} else {
$read = self::readBytes(
$inputHandle,
Core::BUFFER_BYTE_SIZE
);
}
/* Recalculate the MAC (so far) and compare it with the one we
* remembered from pass #1 to ensure attackers didn't change the
* ciphertext after MAC verification. */
\hash_update($hmac2, $read);
$calc_mac = \hash_copy($hmac2);
if ($calc_mac === false) {
throw new Ex\EnvironmentIsBrokenException(
'Cannot duplicate a hash context'
);
}
$calc = \hash_final($calc_mac);
if (empty($macs)) {
throw new Ex\WrongKeyOrModifiedCiphertextException(
'File was modified after MAC verification'
);
} elseif (! Core::hashEquals(\array_shift($macs), $calc)) {
throw new Ex\WrongKeyOrModifiedCiphertextException(
'File was modified after MAC verification'
);
}
/* Decrypt this buffer-sized chunk. */
$decrypted = \openssl_decrypt(
$read,
Core::CIPHER_METHOD,
$ekey,
OPENSSL_RAW_DATA,
$thisIv
);
if ($decrypted === false) {
throw new Ex\EnvironmentIsBrokenException(
'OpenSSL decryption error'
);
}
/* Write the plaintext to the output file. */
self::writeBytes(
$outputHandle,
$decrypted,
Core::ourStrlen($decrypted)
);
/* Increment the IV by the amount of blocks in a buffer. */
$thisIv = Core::incrementCounter($thisIv, $inc);
/* WARNING: Usually, unless the file is a multiple of the buffer
* size, $thisIv will contain an incorrect value here on the last
* iteration of this loop. */
}
}
/**
* Read from a stream; prevent partial reads.
*
* @param resource $stream
* @param int $num_bytes
*
* @throws Ex\IOException
* @throws Ex\EnvironmentIsBrokenException
*
* @return string
*/
public static function readBytes($stream, $num_bytes)
{
if ($num_bytes < 0) {
throw new Ex\EnvironmentIsBrokenException(
'Tried to read less than 0 bytes'
);
} elseif ($num_bytes === 0) {
return '';
}
$buf = '';
$remaining = $num_bytes;
while ($remaining > 0 && ! \feof($stream)) {
$read = \fread($stream, $remaining);
if ($read === false) {
throw new Ex\IOException(
'Could not read from the file'
);
}
$buf .= $read;
$remaining -= Core::ourStrlen($read);
}
if (Core::ourStrlen($buf) !== $num_bytes) {
throw new Ex\IOException(
'Tried to read past the end of the file'
);
}
return $buf;
}
/**
* Write to a stream; prevents partial writes.
*
* @param resource $stream
* @param string $buf
* @param int $num_bytes
*
* @throws Ex\IOException
*
* @return string
*/
public static function writeBytes($stream, $buf, $num_bytes = null)
{
$bufSize = Core::ourStrlen($buf);
if ($num_bytes === null) {
$num_bytes = $bufSize;
}
if ($num_bytes > $bufSize) {
throw new Ex\IOException(
'Trying to write more bytes than the buffer contains.'
);
}
if ($num_bytes < 0) {
throw new Ex\IOException(
'Tried to write less than 0 bytes'
);
}
$remaining = $num_bytes;
while ($remaining > 0) {
$written = \fwrite($stream, $buf, $remaining);
if ($written === false) {
throw new Ex\IOException(
'Could not write to the file'
);
}
$buf = Core::ourSubstr($buf, $written, null);
$remaining -= $written;
}
return $num_bytes;
}
/**
* Returns the last PHP error's or warning's message string.
*
* @return string
*/
private static function getLastErrorMessage()
{
$error = error_get_last();
if ($error === null) {
return '[no PHP error]';
} else {
return $error['message'];
}
}
}

View File

@@ -0,0 +1,84 @@
<?php
namespace Defuse\Crypto;
use Defuse\Crypto\Exception as Ex;
final class Key
{
const KEY_CURRENT_VERSION = "\xDE\xF0\x00\x00";
const KEY_BYTE_SIZE = 32;
private $key_bytes = null;
/**
* Creates new random key.
*
* @throws Ex\EnvironmentIsBrokenException
*
* @return Key
*/
public static function createNewRandomKey()
{
return new Key(Core::secureRandom(self::KEY_BYTE_SIZE));
}
/**
* Loads a Key from its encoded form.
*
* @param string $saved_key_string
*
* @throws Ex\BadFormatException
* @throws Ex\EnvironmentIsBrokenException
*
* @return Key
*/
public static function loadFromAsciiSafeString($saved_key_string)
{
$key_bytes = Encoding::loadBytesFromChecksummedAsciiSafeString(self::KEY_CURRENT_VERSION, $saved_key_string);
return new Key($key_bytes);
}
/**
* Encodes the Key into a string of printable ASCII characters.
*
* @throws Ex\EnvironmentIsBrokenException
*
* @return string
*/
public function saveToAsciiSafeString()
{
return Encoding::saveBytesToChecksummedAsciiSafeString(
self::KEY_CURRENT_VERSION,
$this->key_bytes
);
}
/**
* Gets the raw bytes of the key.
*
* @return string
*/
public function getRawBytes()
{
return $this->key_bytes;
}
/**
* Constructs a new Key object from a string of raw bytes.
*
* @param string $bytes
*
* @throws Ex\EnvironmentIsBrokenException
*/
private function __construct($bytes)
{
if (Core::ourStrlen($bytes) !== self::KEY_BYTE_SIZE) {
throw new Ex\EnvironmentIsBrokenException(
'Bad key length.'
);
}
$this->key_bytes = $bytes;
}
}

View File

@@ -0,0 +1,119 @@
<?php
namespace Defuse\Crypto;
use Defuse\Crypto\Exception as Ex;
final class KeyOrPassword
{
const PBKDF2_ITERATIONS = 100000;
const SECRET_TYPE_KEY = 1;
const SECRET_TYPE_PASSWORD = 2;
private $secret_type = null;
private $secret = null;
/**
* Initializes an instance of KeyOrPassword from a key.
*
* @param Key $key
*
* @return KeyOrPassword
*/
public static function createFromKey(Key $key)
{
return new KeyOrPassword(self::SECRET_TYPE_KEY, $key);
}
/**
* Initializes an instance of KeyOrPassword from a password.
*
* @param string $password
*
* @return KeyOrPassword
*/
public static function createFromPassword($password)
{
return new KeyOrPassword(self::SECRET_TYPE_PASSWORD, $password);
}
/**
* Derives authentication and encryption keys from the secret, using a slow
* key derivation function if the secret is a password.
*
* @param string $salt
*
* @throws Ex\EnvironmentIsBrokenException
*
* @return DerivedKeys
*/
public function deriveKeys($salt)
{
if (Core::ourStrlen($salt) !== Core::SALT_BYTE_SIZE) {
throw new Ex\EnvironmentIsBrokenException('Bad salt.');
}
if ($this->secret_type === self::SECRET_TYPE_KEY) {
$akey = Core::HKDF(
Core::HASH_FUNCTION_NAME,
$this->secret->getRawBytes(),
Core::KEY_BYTE_SIZE,
Core::AUTHENTICATION_INFO_STRING,
$salt
);
$ekey = Core::HKDF(
Core::HASH_FUNCTION_NAME,
$this->secret->getRawBytes(),
Core::KEY_BYTE_SIZE,
Core::ENCRYPTION_INFO_STRING,
$salt
);
return new DerivedKeys($akey, $ekey);
} elseif ($this->secret_type === self::SECRET_TYPE_PASSWORD) {
/* Our PBKDF2 polyfill is vulnerable to a DoS attack documented in
* GitHub issue #230. The fix is to pre-hash the password to ensure
* it is short. We do the prehashing here instead of in pbkdf2() so
* that pbkdf2() still computes the function as defined by the
* standard. */
$prehash = \hash(Core::HASH_FUNCTION_NAME, $this->secret, true);
$prekey = Core::pbkdf2(
Core::HASH_FUNCTION_NAME,
$prehash,
$salt,
self::PBKDF2_ITERATIONS,
Core::KEY_BYTE_SIZE,
true
);
$akey = Core::HKDF(
Core::HASH_FUNCTION_NAME,
$prekey,
Core::KEY_BYTE_SIZE,
Core::AUTHENTICATION_INFO_STRING,
$salt
);
/* Note the cryptographic re-use of $salt here. */
$ekey = Core::HKDF(
Core::HASH_FUNCTION_NAME,
$prekey,
Core::KEY_BYTE_SIZE,
Core::ENCRYPTION_INFO_STRING,
$salt
);
return new DerivedKeys($akey, $ekey);
} else {
throw new Ex\EnvironmentIsBrokenException('Bad secret type.');
}
}
/**
* Constructor for KeyOrPassword.
*
* @param int $secret_type
* @param mixed $secret (either a Key or a password string)
*/
private function __construct($secret_type, $secret)
{
$this->secret_type = $secret_type;
$this->secret = $secret;
}
}

View File

@@ -0,0 +1,112 @@
<?php
namespace Defuse\Crypto;
use Defuse\Crypto\Exception as Ex;
final class KeyProtectedByPassword
{
const PASSWORD_KEY_CURRENT_VERSION = "\xDE\xF1\x00\x00";
private $encrypted_key = null;
/**
* Creates a random key protected by the provided password.
*
* @param string $password
*
* @throws Ex\EnvironmentIsBrokenException
*
* @return KeyProtectedByPassword
*/
public static function createRandomPasswordProtectedKey($password)
{
$inner_key = Key::createNewRandomKey();
/* The password is hashed as a form of poor-man's domain separation
* between this use of encryptWithPassword() and other uses of
* encryptWithPassword() that the user may also be using as part of the
* same protocol. */
$encrypted_key = Crypto::encryptWithPassword(
$inner_key->saveToAsciiSafeString(),
\hash(Core::HASH_FUNCTION_NAME, $password, true),
true
);
return new KeyProtectedByPassword($encrypted_key);
}
/**
* Loads a KeyProtectedByPassword from its encoded form.
*
* @param string $saved_key_string
*
* @throws Ex\BadFormatException
*
* @return KeyProtectedByPassword
*/
public static function loadFromAsciiSafeString($saved_key_string)
{
$encrypted_key = Encoding::loadBytesFromChecksummedAsciiSafeString(
self::PASSWORD_KEY_CURRENT_VERSION,
$saved_key_string
);
return new KeyProtectedByPassword($encrypted_key);
}
/**
* Encodes the KeyProtectedByPassword into a string of printable ASCII
* characters.
*
* @throws Ex\EnvironmentIsBrokenException
*
* @return string
*/
public function saveToAsciiSafeString()
{
return Encoding::saveBytesToChecksummedAsciiSafeString(
self::PASSWORD_KEY_CURRENT_VERSION,
$this->encrypted_key
);
}
/**
* Decrypts the protected key, returning an unprotected Key object that can
* be used for encryption and decryption.
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\WrongKeyOrModifiedCiphertextException
*
* @return Key
*/
public function unlockKey($password)
{
try {
$inner_key_encoded = Crypto::decryptWithPassword(
$this->encrypted_key,
\hash(Core::HASH_FUNCTION_NAME, $password, true),
true
);
return Key::loadFromAsciiSafeString($inner_key_encoded);
} catch (Ex\BadFormatException $ex) {
/* This should never happen unless an attacker replaced the
* encrypted key ciphertext with some other ciphertext that was
* encrypted with the same password. We transform the exception type
* here in order to make the API simpler, avoiding the need to
* document that this method might throw an Ex\BadFormatException. */
throw new Ex\WrongKeyOrModifiedCiphertextException(
"The decrypted key was found to be in an invalid format. " .
"This very likely indicates it was modified by an attacker."
);
}
}
/**
* Constructor for KeyProtectedByPassword.
*
* @param string $encrypted_key
*/
private function __construct($encrypted_key)
{
$this->encrypted_key = $encrypted_key;
}
}

View File

@@ -0,0 +1,242 @@
<?php
namespace Defuse\Crypto;
use Defuse\Crypto\Exception as Ex;
/*
* We're using static class inheritance to get access to protected methods
* inside Crypto. To make it easy to know where the method we're calling can be
* found, within this file, prefix calls with `Crypto::` or `RuntimeTests::`,
* and don't use `self::`.
*/
class RuntimeTests extends Crypto
{
/**
* Runs the runtime tests.
*
* @throws Ex\EnvironmentIsBrokenException
*/
public static function runtimeTest()
{
// 0: Tests haven't been run yet.
// 1: Tests have passed.
// 2: Tests are running right now.
// 3: Tests have failed.
static $test_state = 0;
if ($test_state === 1 || $test_state === 2) {
return;
}
if ($test_state === 3) {
/* If an intermittent problem caused a test to fail previously, we
* want that to be indicated to the user with every call to this
* library. This way, if the user first does something they really
* don't care about, and just ignores all exceptions, they won't get
* screwed when they then start to use the library for something
* they do care about. */
throw new Ex\EnvironmentIsBrokenException('Tests failed previously.');
}
try {
$test_state = 2;
Core::ensureFunctionExists('openssl_get_cipher_methods');
if (\in_array(Core::CIPHER_METHOD, \openssl_get_cipher_methods()) === false) {
throw new Ex\EnvironmentIsBrokenException(
'Cipher method not supported. This is normally caused by an outdated ' .
'version of OpenSSL (and/or OpenSSL compiled for FIPS compliance). ' .
'Please upgrade to a newer version of OpenSSL that supports ' .
Core::CIPHER_METHOD . ' to use this library.'
);
}
RuntimeTests::AESTestVector();
RuntimeTests::HMACTestVector();
RuntimeTests::HKDFTestVector();
RuntimeTests::testEncryptDecrypt();
if (Core::ourStrlen(Key::createNewRandomKey()->getRawBytes()) != Core::KEY_BYTE_SIZE) {
throw new Ex\EnvironmentIsBrokenException();
}
if (Core::ENCRYPTION_INFO_STRING == Core::AUTHENTICATION_INFO_STRING) {
throw new Ex\EnvironmentIsBrokenException();
}
} catch (Ex\EnvironmentIsBrokenException $ex) {
// Do this, otherwise it will stay in the "tests are running" state.
$test_state = 3;
throw $ex;
}
// Change this to '0' make the tests always re-run (for benchmarking).
$test_state = 1;
}
/**
* High-level tests of Crypto operations.
*
* @throws Ex\EnvironmentIsBrokenException
*/
private static function testEncryptDecrypt()
{
$key = Key::createNewRandomKey();
$data = "EnCrYpT EvErYThInG\x00\x00";
// Make sure encrypting then decrypting doesn't change the message.
$ciphertext = Crypto::encrypt($data, $key, true);
try {
$decrypted = Crypto::decrypt($ciphertext, $key, true);
} catch (Ex\WrongKeyOrModifiedCiphertextException $ex) {
// It's important to catch this and change it into a
// Ex\EnvironmentIsBrokenException, otherwise a test failure could trick
// the user into thinking it's just an invalid ciphertext!
throw new Ex\EnvironmentIsBrokenException();
}
if ($decrypted !== $data) {
throw new Ex\EnvironmentIsBrokenException();
}
// Modifying the ciphertext: Appending a string.
try {
Crypto::decrypt($ciphertext . 'a', $key, true);
throw new Ex\EnvironmentIsBrokenException();
} catch (Ex\WrongKeyOrModifiedCiphertextException $e) { /* expected */
}
// Modifying the ciphertext: Changing an HMAC byte.
$indices_to_change = [
0, // The header.
Core::HEADER_VERSION_SIZE + 1, // the salt
Core::HEADER_VERSION_SIZE + Core::SALT_BYTE_SIZE + 1, // the IV
Core::HEADER_VERSION_SIZE + Core::SALT_BYTE_SIZE + Core::BLOCK_BYTE_SIZE + 1, // the ciphertext
];
foreach ($indices_to_change as $index) {
try {
$ciphertext[$index] = \chr((\ord($ciphertext[$index]) + 1) % 256);
Crypto::decrypt($ciphertext, $key, true);
throw new Ex\EnvironmentIsBrokenException();
} catch (Ex\WrongKeyOrModifiedCiphertextException $e) { /* expected */
}
}
// Decrypting with the wrong key.
$key = Key::createNewRandomKey();
$data = 'abcdef';
$ciphertext = Crypto::encrypt($data, $key, true);
$wrong_key = Key::createNewRandomKey();
try {
Crypto::decrypt($ciphertext, $wrong_key, true);
throw new Ex\EnvironmentIsBrokenException();
} catch (Ex\WrongKeyOrModifiedCiphertextException $e) { /* expected */
}
// Ciphertext too small.
$key = Key::createNewRandomKey();
$ciphertext = \str_repeat('A', Core::MINIMUM_CIPHERTEXT_SIZE - 1);
try {
Crypto::decrypt($ciphertext, $key, true);
throw new Ex\EnvironmentIsBrokenException();
} catch (Ex\WrongKeyOrModifiedCiphertextException $e) { /* expected */
}
}
/**
* Test HKDF against test vectors.
*
* @throws Ex\EnvironmentIsBrokenException
*/
private static function HKDFTestVector()
{
// HKDF test vectors from RFC 5869
// Test Case 1
$ikm = \str_repeat("\x0b", 22);
$salt = Encoding::hexToBin('000102030405060708090a0b0c');
$info = Encoding::hexToBin('f0f1f2f3f4f5f6f7f8f9');
$length = 42;
$okm = Encoding::hexToBin(
'3cb25f25faacd57a90434f64d0362f2a' .
'2d2d0a90cf1a5a4c5db02d56ecc4c5bf' .
'34007208d5b887185865'
);
$computed_okm = Core::HKDF('sha256', $ikm, $length, $info, $salt);
if ($computed_okm !== $okm) {
throw new Ex\EnvironmentIsBrokenException();
}
// Test Case 7
$ikm = \str_repeat("\x0c", 22);
$length = 42;
$okm = Encoding::hexToBin(
'2c91117204d745f3500d636a62f64f0a' .
'b3bae548aa53d423b0d1f27ebba6f5e5' .
'673a081d70cce7acfc48'
);
$computed_okm = Core::HKDF('sha1', $ikm, $length, '', null);
if ($computed_okm !== $okm) {
throw new Ex\EnvironmentIsBrokenException();
}
}
/**
* Test HMAC against test vectors.
*
* @throws Ex\EnvironmentIsBrokenException
*/
private static function HMACTestVector()
{
// HMAC test vector From RFC 4231 (Test Case 1)
$key = \str_repeat("\x0b", 20);
$data = 'Hi There';
$correct = 'b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7';
if (\hash_hmac(Core::HASH_FUNCTION_NAME, $data, $key) !== $correct) {
throw new Ex\EnvironmentIsBrokenException();
}
}
/**
* Test AES against test vectors.
*
* @throws Ex\EnvironmentIsBrokenException
*/
private static function AESTestVector()
{
// AES CTR mode test vector from NIST SP 800-38A
$key = Encoding::hexToBin(
'603deb1015ca71be2b73aef0857d7781' .
'1f352c073b6108d72d9810a30914dff4'
);
$iv = Encoding::hexToBin('f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff');
$plaintext = Encoding::hexToBin(
'6bc1bee22e409f96e93d7e117393172a' .
'ae2d8a571e03ac9c9eb76fac45af8e51' .
'30c81c46a35ce411e5fbc1191a0a52ef' .
'f69f2445df4f9b17ad2b417be66c3710'
);
$ciphertext = Encoding::hexToBin(
'601ec313775789a5b7a7f504bbf3d228' .
'f443e3ca4d62b59aca84e990cacaf5c5' .
'2b0930daa23de94ce87017ba2d84988d' .
'dfc9c58db67aada613c2dd08457941a6'
);
$computed_ciphertext = Crypto::plainEncrypt($plaintext, $key, $iv);
if ($computed_ciphertext !== $ciphertext) {
echo \str_repeat("\n", 30);
echo \bin2hex($computed_ciphertext);
echo "\n---\n";
echo \bin2hex($ciphertext);
echo \str_repeat("\n", 30);
throw new Ex\EnvironmentIsBrokenException();
}
$computed_plaintext = Crypto::plainDecrypt($ciphertext, $key, $iv, Core::CIPHER_METHOD);
if ($computed_plaintext !== $plaintext) {
throw new Ex\EnvironmentIsBrokenException();
}
}
}

View File

@@ -540,7 +540,7 @@ abstract class Base
*
* Where $hash (default = sha1) currently supports the following hashes: see: Crypt/Hash.php
*
* @see Crypt/Hash.php
* @see OldCrypt/Hash.php
* @param string $password
* @param string $method
* @throws \LengthException if pbkdf1 is being used and the derived key length exceeds the hash length

View File

@@ -26,10 +26,11 @@ namespace Plugins\Authenticator;
use InvalidArgumentException;
use SP\Controller\TabControllerBase;
use SP\Core\Crypt;
use SP\Core\OldCrypt;
use SP\Core\Plugin\PluginBase;
use SP\Core\Plugin\PluginInterface;
use SP\Util\ArrayUtil;
use SP\Util\Util;
/**
* Class Controller
@@ -79,7 +80,7 @@ class PreferencesController
$IV = null;
if (!$AuthenticatorData->isTwofaEnabled()) {
$IV = Crypt::getIV();
$IV = Util::generateRandomBytes();
$AuthenticatorData->setIV($IV);
Session::setUserData($AuthenticatorData);

View File

@@ -24,7 +24,7 @@
namespace SP\Account;
use SP\Core\Crypt;
use SP\Core\OldCrypt;
use SP\Core\Exceptions\SPException;
use SP\Core\Session;
use SP\DataModel\AccountData;
@@ -303,10 +303,10 @@ class Account extends AccountBase implements AccountInterface
*/
protected function setPasswordEncrypted($masterPass = null)
{
$pass = Crypt::encryptData($this->accountData->getAccountPass(), $masterPass);
$securedKey = Crypt\Crypt::makeSecuredKey($masterPass);
$this->accountData->setAccountPass($pass['data']);
$this->accountData->setAccountIV($pass['iv']);
$this->accountData->setAccountPass(Crypt\Crypt::encrypt($this->accountData->getAccountPass(), $securedKey));
$this->accountData->setAccountIV($securedKey);
}
/**
@@ -381,109 +381,6 @@ class Account extends AccountBase implements AccountInterface
return DB::getQuery($Data);
}
/**
* Actualiza las claves de todas las cuentas con la nueva clave maestra.
*
* @param string $currentMasterPass con la clave maestra actual
* @param string $newMasterPass con la nueva clave maestra
* @param string $newHash con el nuevo hash de la clave maestra
* @return bool
* @throws \phpmailer\phpmailerException
* @throws \SP\Core\Exceptions\SPException
*/
public function updateAccountsMasterPass($currentMasterPass, $newMasterPass, $newHash = null)
{
$accountsOk = [];
$userId = Session::getUserData()->getUserId();
$demoEnabled = Checks::demoIsEnabled();
$errorCount = 0;
$Log = new Log();
$LogMessage = $Log->getLogMessage();
$LogMessage->setAction(__('Actualizar Clave Maestra', false));
$LogMessage->addDescription(__('Inicio', false));
$Log->writeLog(true);
if (!Crypt::checkCryptModule()) {
$LogMessage->addDescription(__('Error en el módulo de encriptación', false));
$Log->setLogLevel(Log::ERROR);
$Log->writeLog();
return false;
}
$accountsPass = $this->getAccountsPassData();
if (!$accountsPass) {
$LogMessage->addDescription(__('Error al obtener las claves de las cuentas', false));
$Log->setLogLevel(Log::ERROR);
$Log->writeLog();
return false;
}
foreach ($accountsPass as $account) {
$this->accountData->setAccountId($account->account_id);
$this->accountData->setAccountUserEditId($userId);
// No realizar cambios si está en modo demo
if ($demoEnabled) {
$accountsOk[] = $this->accountData->getAccountId();
continue;
}
if (empty($account->account_pass)) {
$LogMessage->addDetails(__('Clave de cuenta vacía', false), sprintf(' % s(%d)', $account->account_name, $account->account_id));
continue;
}
if (strlen($account->account_IV) < 32) {
$LogMessage->addDetails(__('IV de encriptación incorrecto', false), sprintf(' % s(%d)', $account->account_name, $account->account_id));
}
$decryptedPass = Crypt::getDecrypt($account->account_pass, $account->account_IV, $currentMasterPass);
$this->accountData->setAccountPass($decryptedPass);
$this->setPasswordEncrypted($newMasterPass);
if ($this->accountData->getAccountPass() === false) {
$errorCount++;
$LogMessage->addDetails(__('No es posible desencriptar la clave de la cuenta', false), sprintf(' % s(%d)', $account->account_name, $account->account_id));
continue;
}
try {
$this->updateAccountPass(true);
$accountsOk[] = $this->accountData->getAccountId();
} catch (SPException $e) {
$errorCount++;
$LogMessage->addDetails(__('Fallo al actualizar la clave de la cuenta', false), sprintf(' % s(%d)', $account->account_name, $account->account_id));
continue;
}
}
$LogMessage->addDetails(__('Cuentas actualizadas', false), implode(',', $accountsOk));
$LogMessage->addDetails(__('Errores', false), $errorCount);
$Log->writeLog();
Email::sendEmail($LogMessage);
return true;
}
/**
* Obtener los datos relativos a la clave de todas las cuentas.
*
* @return array Con los datos de la clave
*/
protected function getAccountsPassData()
{
$query = /** @lang SQL */
'SELECT account_id, account_name, account_pass, account_IV FROM accounts';
$Data = new QueryData();
$Data->setQuery($query);
return DB::getResultsArray($Data);
}
/**
* Actualiza la clave de una cuenta en la BBDD.
*

View File

@@ -108,9 +108,4 @@ abstract class AccountBase
* Esta funcion realiza la consulta a la BBDD y devuelve los datos.
*/
protected abstract function getAccountPassData();
/**
* Obtener los datos relativos a la clave de todas las cuentas.
*/
protected abstract function getAccountsPassData();
}

View File

@@ -0,0 +1,221 @@
<?php
/**
* sysPass
*
* @author nuxsmin
* @link http://syspass.org
* @copyright 2012-2017, Rubén Domínguez nuxsmin@$syspass.org
*
* This file is part of sysPass.
*
* sysPass is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* sysPass is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with sysPass. If not, see <http://www.gnu.org/licenses/>.
*/
namespace SP\Account;
use SP\Core\OldCrypt;
use SP\Core\Exceptions\SPException;
use SP\Core\Session;
use SP\DataModel\AccountData;
use SP\Log\Email;
use SP\Log\Log;
use SP\Storage\DB;
use SP\Storage\QueryData;
use SP\Util\Checks;
/**
* Class AccountCrypt
*
* @package SP\Account
*/
class AccountCrypt
{
/**
* Actualiza las claves de todas las cuentas con la clave maestra actual
* usando nueva encriptación.
*
* @param $currentMasterPass
* @return bool
* @throws \Defuse\Crypto\Exception\BadFormatException
* @throws \Defuse\Crypto\Exception\EnvironmentIsBrokenException
*/
public function updateOldPass(&$currentMasterPass)
{
$accountsOk = [];
$userId = Session::getUserData()->getUserId();
$demoEnabled = Checks::demoIsEnabled();
$errorCount = 0;
$Log = new Log();
$LogMessage = $Log->getLogMessage();
$LogMessage->setAction(__('Actualizar Clave Maestra', false));
$LogMessage->addDescription(__('Inicio', false));
$Log->writeLog(true);
if (!OldCrypt::checkCryptModule()) {
$LogMessage->addDescription(__('Error en el módulo de encriptación', false));
$Log->setLogLevel(Log::ERROR);
$Log->writeLog();
return false;
}
$accountsPass = $this->getAccountsPassData();
if (count($accountsPass) === 0) {
$LogMessage->addDescription(__('Error al obtener las claves de las cuentas', false));
$Log->setLogLevel(Log::ERROR);
$Log->writeLog();
return false;
}
$AccountDataBase = new AccountData();
foreach ($accountsPass as $account) {
$AccountData = clone $AccountDataBase;
$AccountData->setAccountId($account->account_id);
$AccountData->setAccountUserEditId($userId);
// No realizar cambios si está en modo demo
if ($demoEnabled) {
$accountsOk[] = $account->account_id;
continue;
} elseif (empty($account->account_pass)) {
$LogMessage->addDetails(__('Clave de cuenta vacía', false), sprintf('%s (%d)', $account->account_name, $account->account_id));
continue;
} elseif (strlen($account->account_IV) < 32) {
$LogMessage->addDetails(__('IV de encriptación incorrecto', false), sprintf('%s (%d)', $account->account_name, $account->account_id));
}
$decryptedPass = OldCrypt::getDecrypt($account->account_pass, $account->account_IV, $currentMasterPass);
$securedKey = Crypt\Crypt::makeSecuredKey($currentMasterPass);
$AccountData->setAccountPass(Crypt\Crypt::encrypt($decryptedPass, $securedKey));
$AccountData->setAccountIV($securedKey);
try {
$Account = new Account($AccountData);
$Account->updateAccountPass(true);
$accountsOk[] = $account->account_id;
} catch (SPException $e) {
$errorCount++;
$LogMessage->addDetails(__('Fallo al actualizar la clave de la cuenta', false), sprintf('%s (%d)', $account->account_name, $account->account_id));
}
}
$LogMessage->addDetails(__('Cuentas actualizadas', false), implode(',', $accountsOk));
$LogMessage->addDetails(__('Errores', false), $errorCount);
$Log->writeLog();
Email::sendEmail($LogMessage);
return true;
}
/**
* Actualiza las claves de todas las cuentas con la nueva clave maestra.
*
* @param $currentMasterPass
* @param $newMasterPass
* @return bool
* @throws \Defuse\Crypto\Exception\BadFormatException
* @throws \Defuse\Crypto\Exception\EnvironmentIsBrokenException
*/
public function updatePass($currentMasterPass, $newMasterPass)
{
$accountsOk = [];
$userId = Session::getUserData()->getUserId();
$demoEnabled = Checks::demoIsEnabled();
$errorCount = 0;
$Log = new Log();
$LogMessage = $Log->getLogMessage();
$LogMessage->setAction(__('Actualizar Clave Maestra', false));
$LogMessage->addDescription(__('Inicio', false));
$Log->writeLog(true);
$accountsPass = $this->getAccountsPassData();
if (count($accountsPass) === 0) {
$LogMessage->addDescription(__('Error al obtener las claves de las cuentas', false));
$Log->setLogLevel(Log::ERROR);
$Log->writeLog();
return false;
}
$AccountDataBase = new AccountData();
foreach ($accountsPass as $account) {
$AccountData = clone $AccountDataBase;
$AccountData->setAccountId($account->account_id);
$AccountData->setAccountUserEditId($userId);
// No realizar cambios si está en modo demo
if ($demoEnabled) {
$accountsOk[] = $account->account_id;
continue;
} elseif (empty($account->account_pass)) {
$LogMessage->addDetails(__('Clave de cuenta vacía', false), sprintf('%s (%d)', $account->account_name, $account->account_id));
continue;
} elseif (strlen($account->account_IV) < 32) {
$LogMessage->addDetails(__('IV de encriptación incorrecto', false), sprintf('%s (%d)', $account->account_name, $account->account_id));
}
$currentSecuredKey = Crypt\Crypt::unlockSecuredKey($account->account_IV, $currentMasterPass);
$decryptedPass = Crypt\Crypt::decrypt($account->account_pass, $currentSecuredKey);
$newSecuredKey = Crypt\Crypt::makeSecuredKey($newMasterPass);
$AccountData->setAccountPass(Crypt\Crypt::encrypt($decryptedPass, $newSecuredKey));
$AccountData->setAccountIV($newSecuredKey);
try {
$Account = new Account($AccountData);
$Account->updateAccountPass(true);
$accountsOk[] = $account->account_id;
} catch (SPException $e) {
$errorCount++;
$LogMessage->addDetails(__('Fallo al actualizar la clave de la cuenta', false), sprintf('%s (%d)', $account->account_name, $account->account_id));
}
}
$LogMessage->addDetails(__('Cuentas actualizadas', false), implode(',', $accountsOk));
$LogMessage->addDetails(__('Errores', false), $errorCount);
$Log->writeLog();
Email::sendEmail($LogMessage);
return true;
}
/**
* Obtener los datos relativos a la clave de todas las cuentas.
*
* @return array Con los datos de la clave
*/
protected function getAccountsPassData()
{
$query = /** @lang SQL */
'SELECT account_id, account_name, account_pass, account_IV FROM accounts';
$Data = new QueryData();
$Data->setQuery($query);
return DB::getResultsArray($Data);
}
}

View File

@@ -25,7 +25,7 @@
namespace SP\Account;
use SP\Config\ConfigDB;
use SP\Core\Crypt;
use SP\Core\OldCrypt;
use SP\Core\Exceptions\SPException;
use SP\Log\Log;
use SP\Storage\DB;
@@ -242,116 +242,6 @@ class AccountHistory extends AccountBase implements AccountInterface
return DB::getQuery($Data);
}
/**
* Actualiza las claves de todas las cuentas en el histórico con la nueva clave maestra.
*
* @param string $currentMasterPass con la clave maestra actual
* @param string $newMasterPass con la nueva clave maestra
* @param string $newHash con el nuevo hash de la clave maestra
* @return bool
* @throws \SP\Core\Exceptions\SPException
*/
public function updateAccountsMasterPass($currentMasterPass, $newMasterPass, $newHash = null)
{
$accountsOk = [];
$errorCount = 0;
$demoEnabled = Checks::demoIsEnabled();
$Log = new Log();
$LogMessage = $Log->getLogMessage();
$LogMessage->setAction(__('Actualizar Clave Maestra (H)', false));
$LogMessage->addDescription(__('Inicio', false));
$Log->writeLog(true);
if (!Crypt::checkCryptModule()) {
$Log->setLogLevel(Log::ERROR);
$LogMessage->addDescription(__('Error en el módulo de encriptación', false));
$Log->writeLog();
return false;
}
$accountsPass = $this->getAccountsPassData();
if (!$accountsPass) {
$Log->setLogLevel(Log::ERROR);
$LogMessage->addDescription(__('Error al obtener las claves de las cuentas', false));
$Log->writeLog();
return false;
}
$AccountDataBase = new \stdClass();
$AccountDataBase->id = 0;
$AccountDataBase->pass = '';
$AccountDataBase->iv = '';
$AccountDataBase->hash = $newHash;
foreach ($accountsPass as $account) {
$AccountData = clone $AccountDataBase;
$AccountData->id = $account->acchistory_id;
// No realizar cambios si está en modo demo
if ($demoEnabled) {
$accountsOk[] = $account->acchistory_id;
continue;
}
if (!$this->checkAccountMPass()) {
$errorCount++;
$LogMessage->addDetails(__('La clave maestra del registro no coincide', false), sprintf('%s (%d)', $account->acchistory_name, $account->acchistory_id));
continue;
}
if ($account->acchistory_pass === '') {
$LogMessage->addDetails(__('Clave de cuenta vacía', false), sprintf('%s (%d)', $account->acchistory_name, $account->acchistory_id));
continue;
}
if (strlen($account->acchistory_IV) < 32) {
$LogMessage->addDetails(__('IV de encriptación incorrecto', false), sprintf('%s (%d)', $account->acchistory_name, $account->acchistory_id));
}
$decryptedPass = Crypt::getDecrypt($account->acchistory_pass, $account->acchistory_IV, $currentMasterPass);
$AccountData->pass = Crypt::mkEncrypt($decryptedPass, $newMasterPass);
$AccountData->iv = Crypt::$strInitialVector;
if ($AccountData->pass === false) {
$errorCount++;
$LogMessage->addDetails(__('No es posible desencriptar la clave de la cuenta', false), sprintf('%s (%d)', $account->acchistory_name, $account->acchistory_id));
continue;
}
try {
$this->updateAccountPass($AccountData);
$accountsOk[] = $account->acchistory_id;
} catch (SPException $e) {
$errorCount++;
$LogMessage->addDetails(__('Fallo al actualizar la clave del histórico', false), sprintf('%s (%d)', $account->acchistory_name, $account->acchistory_id));
}
}
$LogMessage->addDetails(__('Cuentas actualizadas', false), implode(',', $accountsOk));
$LogMessage->addDetails(__('Errores', false), $errorCount);
$Log->writeLog();
return true;
}
/**
* Obtener los datos relativos a la clave de todas las cuentas del histórico.
*
* @return false|array con los datos de la clave
*/
protected function getAccountsPassData()
{
$query = /** @lang SQL */
'SELECT acchistory_id, acchistory_name, acchistory_pass, acchistory_IV FROM accHistory';
$Data = new QueryData();
$Data->setQuery($query);
return DB::getResultsArray($Data);
}
/**
* Comprueba el hash de la clave maestra del registro de histórico de una cuenta.
*

View File

@@ -0,0 +1,235 @@
<?php
/**
* sysPass
*
* @author nuxsmin
* @link http://syspass.org
* @copyright 2012-2017, Rubén Domínguez nuxsmin@$syspass.org
*
* This file is part of sysPass.
*
* sysPass is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* sysPass is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with sysPass. If not, see <http://www.gnu.org/licenses/>.
*/
namespace SP\Account;
use SP\Config\ConfigDB;
use SP\Core\OldCrypt;
use SP\Core\Exceptions\SPException;
use SP\Log\Email;
use SP\Log\Log;
use SP\Storage\DB;
use SP\Storage\QueryData;
use SP\Util\Checks;
/**
* Class AccountHistoryCrypt
*
* @package SP\Account
*/
class AccountHistoryCrypt
{
/**
* Actualiza las claves de todas las cuentas con la clave maestra actual
* usando nueva encriptación.
*
* @param $currentMasterPass
* @return bool
* @throws \Defuse\Crypto\Exception\BadFormatException
* @throws \Defuse\Crypto\Exception\EnvironmentIsBrokenException
*/
public function updateOldPass(&$currentMasterPass)
{
$accountsOk = [];
$demoEnabled = Checks::demoIsEnabled();
$errorCount = 0;
$Log = new Log();
$LogMessage = $Log->getLogMessage();
$LogMessage->setAction(__('Actualizar Clave Maestra (H)', false));
$LogMessage->addDescription(__('Inicio', false));
$Log->writeLog(true);
if (!OldCrypt::checkCryptModule()) {
$LogMessage->addDescription(__('Error en el módulo de encriptación', false));
$Log->setLogLevel(Log::ERROR);
$Log->writeLog();
return false;
}
$accountsPass = $this->getAccountsPassData();
if (count($accountsPass) === 0) {
$LogMessage->addDescription(__('Error al obtener las claves de las cuentas', false));
$Log->setLogLevel(Log::ERROR);
$Log->writeLog();
return false;
}
$currentMPassHash = ConfigDB::getValue('masterPwd');
$AccountDataBase = new \stdClass();
$AccountDataBase->id = 0;
$AccountDataBase->pass = '';
$AccountDataBase->iv = '';
$AccountDataBase->hash = Crypt\Hash::hashKey($currentMasterPass);
foreach ($accountsPass as $account) {
$AccountData = clone $AccountDataBase;
$AccountData->id = $account->acchistory_id;
// No realizar cambios si está en modo demo
if ($demoEnabled) {
$accountsOk[] = $account->acchistory_id;
continue;
} elseif ($account->acchistory_mPassHash !== $currentMPassHash) {
$errorCount++;
$LogMessage->addDetails(__('La clave maestra del registro no coincide', false), sprintf('%s (%d)', $account->acchistory_name, $account->acchistory_id));
continue;
} elseif (empty($account->acchistory_pass)) {
$LogMessage->addDetails(__('Clave de cuenta vacía', false), sprintf('%s (%d)', $account->acchistory_name, $account->acchistory_id));
continue;
} elseif (strlen($account->acchistory_IV) < 32) {
$LogMessage->addDetails(__('IV de encriptación incorrecto', false), sprintf('%s (%d)', $account->acchistory_name, $account->acchistory_id));
}
$decryptedPass = OldCrypt::getDecrypt($account->acchistory_pass, $account->acchistory_IV, $currentMasterPass);
$securedKey = Crypt\Crypt::makeSecuredKey($currentMasterPass);
$AccountData->pass = Crypt\Crypt::encrypt($decryptedPass, $securedKey);
$AccountData->iv = $securedKey;
try {
$Account = new AccountHistory();
$Account->updateAccountPass($AccountData);
$accountsOk[] = $account->acchistory_id;
} catch (SPException $e) {
$errorCount++;
$LogMessage->addDetails(__('Fallo al actualizar la clave del histórico', false), sprintf('%s (%d)', $account->acchistory_name, $account->acchistory_id));
}
}
$LogMessage->addDetails(__('Cuentas actualizadas', false), implode(',', $accountsOk));
$LogMessage->addDetails(__('Errores', false), $errorCount);
$Log->writeLog();
Email::sendEmail($LogMessage);
return true;
}
/**
* Obtener los datos relativos a la clave de todas las cuentas.
*
* @return array Con los datos de la clave
*/
protected function getAccountsPassData()
{
$query = /** @lang SQL */
'SELECT acchistory_id, acchistory_name, acchistory_pass, acchistory_IV, acchistory_mPassHash FROM accHistory';
$Data = new QueryData();
$Data->setQuery($query);
return DB::getResultsArray($Data);
}
/**
* Actualiza las claves de todas las cuentas con la nueva clave maestra.
*
* @param $currentMasterPass
* @param $newMasterPass
* @return bool
* @throws \Defuse\Crypto\Exception\BadFormatException
* @throws \Defuse\Crypto\Exception\EnvironmentIsBrokenException
*/
public function updatePass($currentMasterPass, $newMasterPass)
{
$accountsOk = [];
$demoEnabled = Checks::demoIsEnabled();
$errorCount = 0;
$Log = new Log();
$LogMessage = $Log->getLogMessage();
$LogMessage->setAction(__('Actualizar Clave Maestra (H)', false));
$LogMessage->addDescription(__('Inicio', false));
$Log->writeLog(true);
$accountsPass = $this->getAccountsPassData();
if (count($accountsPass) === 0) {
$LogMessage->addDescription(__('Error al obtener las claves de las cuentas', false));
$Log->setLogLevel(Log::ERROR);
$Log->writeLog();
return false;
}
$currentMPassHash = ConfigDB::getValue('masterPwd');
$AccountDataBase = new \stdClass();
$AccountDataBase->id = 0;
$AccountDataBase->pass = '';
$AccountDataBase->iv = '';
$AccountDataBase->hash = Crypt\Hash::hashKey($newMasterPass);
foreach ($accountsPass as $account) {
$AccountData = clone $AccountDataBase;
$AccountData->id = $account->acchistory_id;
// No realizar cambios si está en modo demo
if ($demoEnabled) {
$accountsOk[] = $account->acchistory_id;
continue;
} elseif ($account->acchistory_mPassHash !== $currentMPassHash) {
$errorCount++;
$LogMessage->addDetails(__('La clave maestra del registro no coincide', false), sprintf('%s (%d)', $account->acchistory_name, $account->acchistory_id));
continue;
} elseif (empty($account->acchistory_pass)) {
$LogMessage->addDetails(__('Clave de cuenta vacía', false), sprintf('%s (%d)', $account->acchistory_name, $account->acchistory_id));
continue;
} elseif (strlen($account->acchistory_IV) < 32) {
$LogMessage->addDetails(__('IV de encriptación incorrecto', false), sprintf('%s (%d)', $account->acchistory_name, $account->acchistory_id));
}
$currentSecuredKey = Crypt\Crypt::unlockSecuredKey($account->acchistory_IV, $currentMasterPass);
$decryptedPass = Crypt\Crypt::decrypt($account->acchistory_pass, $currentSecuredKey);
$newSecuredKey = Crypt\Crypt::makeSecuredKey($newMasterPass);
$AccountData->acchistory_pass = Crypt\Crypt::encrypt($decryptedPass, $newSecuredKey);
$AccountData->acchistory_IV = $newSecuredKey;
try {
$Account = new AccountHistory();
$Account->updateAccountPass($AccountData);
$accountsOk[] = $account->acchistory_id;
} catch (SPException $e) {
$errorCount++;
$LogMessage->addDetails(__('Fallo al actualizar la clave del histórico', false), sprintf('%s (%d)', $account->acchistory_name, $account->acchistory_id));
}
}
$LogMessage->addDetails(__('Cuentas actualizadas', false), implode(',', $accountsOk));
$LogMessage->addDetails(__('Errores', false), $errorCount);
$Log->writeLog();
Email::sendEmail($LogMessage);
return true;
}
}

View File

@@ -51,12 +51,4 @@ interface AccountInterface
* @return mixed
*/
public function deleteAccount($id);
/**
* @param $currentMasterPass
* @param $newMasterPass
* @param null $newHash
* @return mixed
*/
public function updateAccountsMasterPass($currentMasterPass, $newMasterPass, $newHash = null);
}

View File

@@ -31,7 +31,7 @@ use SP\Account\AccountUtil;
use SP\Core\Acl;
use SP\Core\ActionsInterface;
use SP\Core\Backup;
use SP\Core\Crypt;
use SP\Core\OldCrypt;
use SP\Core\Exceptions\SPException;
use SP\DataModel\AccountExtData;
use SP\DataModel\CategoryData;
@@ -84,9 +84,11 @@ class SyspassApi extends ApiBase
$LogMessage->addDetails(__('Origen', false), 'API');
$this->Log->writeLog();
$securedKey = Crypt\Crypt::unlockSecuredKey($AccountData->getAccountIV(), $this->mPass);
$ret = [
'itemId' => $accountId,
'pass' => Crypt::getDecrypt($AccountData->getAccountPass(), $AccountData->getAccountIV(), $this->mPass)
'pass' => Crypt\Crypt::decrypt($AccountData->getAccountPass(), $securedKey)
];
if ($this->getParam('details', false, 0)) {

View File

@@ -34,7 +34,7 @@ use SP\Account\UserAccounts;
use SP\Config\Config;
use SP\Core\Acl;
use SP\Core\ActionsInterface;
use SP\Core\Crypt;
use SP\Core\OldCrypt;
use SP\Core\Exceptions\SPException;
use SP\Core\Init;
use SP\Core\Session;
@@ -149,10 +149,12 @@ class AccountController extends ControllerBase implements ActionsInterface
$this->Account->incrementDecryptCounter();
$AccountPassData = $this->Account->getAccountPassData();
// Obtener la llave de la clave maestra
$securedKey = Crypt\Crypt::unlockSecuredKey($PublicLinkData->getPassIV(), Config::getConfig()->getPasswordSalt() . $PublicLinkData->getLinkHash());
// Desencriptar la clave de la cuenta
$pass = Crypt::generateAesKey($PublicLinkData->getLinkHash());
$masterPass = Crypt::getDecrypt($PublicLinkData->getPass(), $PublicLinkData->getPassIV(), $pass);
$accountPass = Crypt::getDecrypt($AccountPassData->getAccountPass(), $AccountPassData->getAccountIV(), $masterPass);
$accountSecuredKey = Crypt\Crypt::unlockSecuredKey($AccountPassData->getAccountIV(), Crypt\Crypt::decrypt($PublicLinkData->getPass(), $securedKey));
$accountPass = Crypt\Crypt::decrypt($AccountPassData->getAccountPass(), $accountSecuredKey);
$this->view->assign('useImage', Config::getConfig()->isPublinksImageEnabled() || Config::getConfig()->isAccountPassToImage());

View File

@@ -25,12 +25,15 @@
namespace SP\Controller;
use SP\Account\Account;
use SP\Account\AccountCrypt;
use SP\Account\AccountHistory;
use SP\Account\AccountHistoryCrypt;
use SP\Config\Config;
use SP\Config\ConfigDB;
use SP\Core\ActionsInterface;
use SP\Core\Backup;
use SP\Core\Crypt;
use SP\Core\OldCrypt;
use SP\Core\Crypt\Hash;
use SP\Core\CryptMasterPass;
use SP\Core\Exceptions\SPException;
use SP\Core\Init;
@@ -502,7 +505,7 @@ class ConfigActionController implements ItemControllerInterface
} elseif ($newMasterPass !== $newMasterPassR) {
$this->JsonResponse->setDescription(__('Las claves maestras no coinciden', false));
return;
} elseif (!Crypt::checkHashPass($currentMasterPass, ConfigDB::getValue('masterPwd'), true)) {
} elseif (!Hash::checkHashKey($currentMasterPass, ConfigDB::getValue('masterPwd'))) {
$this->JsonResponse->setDescription(__('La clave maestra actual no coincide', false));
return;
}
@@ -512,7 +515,7 @@ class ConfigActionController implements ItemControllerInterface
return;
}
$hashMPass = Crypt::mkHashPassword($newMasterPass);
$hashMPass = Hash::hashKey($newMasterPass);
if (!$noAccountPassChange) {
Util::lockApp();
@@ -522,18 +525,18 @@ class ConfigActionController implements ItemControllerInterface
return;
}
$Account = new Account();
$Account = new AccountCrypt();
if (!$Account->updateAccountsMasterPass($currentMasterPass, $newMasterPass)) {
if (!$Account->updatePass($currentMasterPass, $newMasterPass)) {
DB::rollbackTransaction();
$this->JsonResponse->setDescription(__('Errores al actualizar las claves de las cuentas', false));
return;
}
$AccountHistory = new AccountHistory();
$AccountHistory = new AccountHistoryCrypt();
if (!$AccountHistory->updateAccountsMasterPass($currentMasterPass, $newMasterPass, $hashMPass)) {
if (!$AccountHistory->updatePass($currentMasterPass, $newMasterPass)) {
DB::rollbackTransaction();
$this->JsonResponse->setDescription(__('Errores al actualizar las claves de las cuentas del histórico', false));
@@ -574,6 +577,9 @@ class ConfigActionController implements ItemControllerInterface
/**
* Regenerar el hash de la clave maestra
*
* @throws \Defuse\Crypto\Exception\BadFormatException
* @throws \Defuse\Crypto\Exception\EnvironmentIsBrokenException
*/
protected function masterPassRefreshAction()
{
@@ -584,7 +590,7 @@ class ConfigActionController implements ItemControllerInterface
$this->LogMessage->setAction(__('Actualizar Clave Maestra', false));
if (ConfigDB::setValue('masterPwd', Crypt::mkHashPassword(SessionUtil::getSessionMPass()))) {
if (ConfigDB::setValue('masterPwd', Hash::hashKey(Crypt\Session::getSessionKey()))) {
$this->LogMessage->addDescription(__('Hash de clave maestra actualizado', false));
$this->JsonResponse->setStatus(0);

View File

@@ -31,7 +31,7 @@ use SP\Account\AccountAcl;
use SP\Account\AccountHistory;
use SP\Api\ApiTokensUtil;
use SP\Core\ActionsInterface;
use SP\Core\Crypt;
use SP\Core\OldCrypt;
use SP\Core\Exceptions\ItemException;
use SP\Core\Plugin\PluginUtil;
use SP\Core\Session;
@@ -513,7 +513,8 @@ class ItemShowController extends ControllerBase implements ActionsInterface, Ite
throw new ItemException(__('Clave maestra actualizada', false) . '<br>' . __('Reinicie la sesión para cambiarla', false));
}
$accountClearPass = Crypt::getDecrypt($AccountData->getAccountPass(), $AccountData->getAccountIV());
$securedKey = Crypt\Crypt::unlockSecuredKey($AccountData->getAccountIV(), Crypt\Session::getSessionKey());
$accountClearPass = Crypt\Crypt::decrypt($AccountData->getAccountPass(), $securedKey);
if (!$isHistory) {
$Account->incrementDecryptCounter();

View File

@@ -30,6 +30,8 @@ use SP\Auth\AuthUtil;
use SP\Auth\Browser\BrowserAuthData;
use SP\Auth\Database\DatabaseAuthData;
use SP\Auth\Ldap\LdapAuthData;
use SP\Core\Crypt\Crypt;
use SP\Core\Crypt\Session as CryptSession;
use SP\Core\CryptMasterPass;
use SP\Core\DiFactory;
use SP\Core\Exceptions\AuthException;
@@ -264,6 +266,8 @@ class LoginController
*
* @throws \SP\Core\Exceptions\SPException
* @throws \SP\Core\Exceptions\AuthException
* @throws \Defuse\Crypto\Exception\BadFormatException
* @throws \Defuse\Crypto\Exception\EnvironmentIsBrokenException
*/
protected function loadMasterPass()
{
@@ -284,7 +288,7 @@ class LoginController
throw new AuthException(SPException::SP_INFO, __('Clave maestra incorrecta', false), '', self::STATUS_INVALID_MASTER_PASS);
} else {
SessionUtil::saveSessionMPass($UserPass->getClearUserMPass());
CryptSession::saveSessionKey($UserPass->getClearUserMPass());
$this->LogMessage->addDescription(__('Clave maestra actualizada', false));
}
@@ -294,7 +298,7 @@ class LoginController
throw new AuthException(SPException::SP_INFO, __('Clave maestra incorrecta', false), '', self::STATUS_INVALID_MASTER_PASS);
} else {
SessionUtil::saveSessionMPass($UserPass->getClearUserMPass());
CryptSession::saveSessionKey($UserPass->getClearUserMPass());
$this->LogMessage->addDescription(__('Clave maestra actualizada', false));
}

View File

@@ -0,0 +1,103 @@
<?php
/**
* sysPass
*
* @author nuxsmin
* @link http://syspass.org
* @copyright 2012-2017, Rubén Domínguez nuxsmin@$syspass.org
*
* This file is part of sysPass.
*
* sysPass is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* sysPass is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with sysPass. If not, see <http://www.gnu.org/licenses/>.
*/
namespace SP\Core\Crypt;
use Defuse\Crypto\Crypto;
use Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException;
use Defuse\Crypto\Key;
use Defuse\Crypto\KeyProtectedByPassword;
/**
* Class Crypt
*
* @package SP\Core\Crypt
*/
class Crypt
{
/**
* Encriptar datos con una clave segura
*
* @param $data
* @param $securedKey
* @return string
* @throws \Defuse\Crypto\Exception\EnvironmentIsBrokenException
* @throws \Defuse\Crypto\Exception\BadFormatException
*/
public static function encrypt($data, $securedKey)
{
$key = Key::loadFromAsciiSafeString($securedKey);
return Crypto::encrypt($data, $key);
}
/**
* Desencriptar datos con una clave segura
*
* @param $data
* @param $securedKey
* @return string
* @throws \Defuse\Crypto\Exception\BadFormatException
* @throws \Defuse\Crypto\Exception\EnvironmentIsBrokenException
*/
public static function decrypt($data, $securedKey)
{
$key = Key::loadFromAsciiSafeString($securedKey);
try {
return Crypto::decrypt($data, $key);
} catch (WrongKeyOrModifiedCiphertextException $e) {
return false;
}
}
/**
* Securiza una clave de seguridad
*
* @param $password
* @return string
* @throws \Defuse\Crypto\Exception\EnvironmentIsBrokenException
*/
public static function makeSecuredKey($password)
{
return KeyProtectedByPassword::createRandomPasswordProtectedKey($password)->saveToAsciiSafeString();
}
/**
* @param $key
* @param $password
* @return string
* @throws \Defuse\Crypto\Exception\EnvironmentIsBrokenException
* @throws \Defuse\Crypto\Exception\BadFormatException
*/
public static function unlockSecuredKey($key, $password)
{
try {
return KeyProtectedByPassword::loadFromAsciiSafeString($key)->unlockKey($password)->saveToAsciiSafeString();
} catch (WrongKeyOrModifiedCiphertextException $e) {
return false;
}
}
}

View File

@@ -0,0 +1,57 @@
<?php
/**
* sysPass
*
* @author nuxsmin
* @link http://syspass.org
* @copyright 2012-2017, Rubén Domínguez nuxsmin@$syspass.org
*
* This file is part of sysPass.
*
* sysPass is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* sysPass is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with sysPass. If not, see <http://www.gnu.org/licenses/>.
*/
namespace SP\Core\Crypt;
/**
* Class Hash
*
* @package SP\Core\Crypt
*/
class Hash
{
/**
* Comprobar el hash de una clave.
*
* @param string $key con la clave a comprobar
* @param string $hash con el hash a comprobar
* @return bool
* @throws \SP\Core\Exceptions\SPException
*/
public static function checkHashKey($key, $hash)
{
return password_verify($key, $hash);
}
/**
* Generar un hash de una clave criptográficamente segura
*
* @param string $key con la clave a 'hashear'
* @return string con el hash de la clave
*/
public static function hashKey($key)
{
return password_hash($key, PASSWORD_BCRYPT);
}
}

View File

@@ -0,0 +1,75 @@
<?php
/**
* sysPass
*
* @author nuxsmin
* @link http://syspass.org
* @copyright 2012-2017, Rubén Domínguez nuxsmin@$syspass.org
*
* This file is part of sysPass.
*
* sysPass is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* sysPass is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with sysPass. If not, see <http://www.gnu.org/licenses/>.
*/
namespace SP\Core\Crypt;
use SP\Core\Session as CoreSession;
/**
* Class Session
*
* @package SP\Core\Crypt
*/
class Session
{
/**
* Devolver la clave maestra de la sesión
*
* @return string
* @throws \Defuse\Crypto\Exception\EnvironmentIsBrokenException
* @throws \Defuse\Crypto\Exception\BadFormatException
*/
public static function getSessionKey()
{
$securedKey = Crypt::unlockSecuredKey(CoreSession::getMPassPwd(), self::getPassword());
return Crypt::decrypt(CoreSession::getMPass(), $securedKey);
}
/**
* Devolver la clave utilizada para generar la llave segura
*
* @return string
*/
private static function getPassword()
{
// FIXME
return session_id() . CoreSession::getUserData()->getUserLogin();
}
/**
* Guardar la clave maestra en la sesión
*
* @param $data
* @throws \Defuse\Crypto\Exception\EnvironmentIsBrokenException
* @throws \Defuse\Crypto\Exception\BadFormatException
*/
public static function saveSessionKey($data)
{
$securedKey = Crypt::makeSecuredKey(self::getPassword());
CoreSession::setMPassPwd($securedKey);
CoreSession::setMPass(Crypt::encrypt($data, $securedKey));
}
}

View File

@@ -2,8 +2,8 @@
/**
* sysPass
*
* @author nuxsmin
* @link http://syspass.org
* @author nuxsmin
* @link http://syspass.org
* @copyright 2012-2017, Rubén Domínguez nuxsmin@$syspass.org
*
* This file is part of sysPass.
@@ -25,6 +25,9 @@
namespace SP\Core;
use SP\Config\ConfigDB;
use SP\Core\Crypt\Session as CryptSession;
use SP\Core\Crypt\Crypt;
use SP\Core\Crypt\Hash;
use SP\Log\Log;
use SP\Util\Util;
@@ -47,20 +50,18 @@ class CryptMasterPass
*
* @param int $maxTime El tiempo máximo de validez de la clave
* @return bool|string
* @throws \Defuse\Crypto\Exception\BadFormatException
* @throws \Defuse\Crypto\Exception\EnvironmentIsBrokenException
*/
public static function setTempMasterPass($maxTime = 14400)
{
// Encriptar la clave maestra con hash aleatorio generado
$randomKey = Crypt::generateAesKey(Util::generateRandomBytes());
$pass = Crypt::mkCustomMPassEncrypt($randomKey, SessionUtil::getSessionMPass());
$randomKey = Util::generateRandomBytes(64);
$securedKey = Crypt::makeSecuredKey($randomKey);
if (!is_array($pass)) {
return false;
}
ConfigDB::setCacheConfigValue('tempmaster_pass', bin2hex($pass[0]));
ConfigDB::setCacheConfigValue('tempmaster_passiv', bin2hex($pass[1]));
ConfigDB::setCacheConfigValue('tempmaster_passhash', Crypt::mkHashPassword($randomKey));
ConfigDB::setCacheConfigValue('tempmaster_pass', Crypt::encrypt(CryptSession::getSessionKey(), $securedKey));
ConfigDB::setCacheConfigValue('tempmaster_passkey', $securedKey);
ConfigDB::setCacheConfigValue('tempmaster_passhash', Hash::hashKey($randomKey));
ConfigDB::setCacheConfigValue('tempmaster_passtime', time());
ConfigDB::setCacheConfigValue('tempmaster_maxtime', time() + $maxTime);
ConfigDB::setCacheConfigValue('tempmaster_attempts', 0);
@@ -97,7 +98,7 @@ class CryptMasterPass
|| $attempts >= self::MAX_ATTEMPTS
) {
ConfigDB::setCacheConfigValue('tempmaster_pass', '');
ConfigDB::setCacheConfigValue('tempmaster_passiv', '');
ConfigDB::setCacheConfigValue('tempmaster_passkey', '');
ConfigDB::setCacheConfigValue('tempmaster_passhash', '');
ConfigDB::setCacheConfigValue('tempmaster_maxtime', 0);
ConfigDB::setCacheConfigValue('tempmaster_attempts', 0);
@@ -108,7 +109,7 @@ class CryptMasterPass
return false;
}
$isValid = Crypt::checkHashPass($pass, ConfigDB::getValue('tempmaster_passhash'));
$isValid = Hash::checkHashKey($pass, ConfigDB::getValue('tempmaster_passhash'));
if (!$isValid) {
ConfigDB::setValue('tempmaster_attempts', $attempts + 1, false);
@@ -120,14 +121,15 @@ class CryptMasterPass
/**
* Devuelve la clave maestra que ha sido encriptada con la clave temporal
*
* @param $pass string con la clave utilizada para encriptar
* @param $randomKey string con la clave utilizada para encriptar
* @return string con la clave maestra desencriptada
* @throws \Defuse\Crypto\Exception\EnvironmentIsBrokenException
* @throws \Defuse\Crypto\Exception\BadFormatException
*/
public static function getTempMasterPass($pass)
public static function getTempMasterPass($randomKey)
{
$passLogin = hex2bin(ConfigDB::getValue('tempmaster_pass'));
$passLoginIV = hex2bin(ConfigDB::getValue('tempmaster_passiv'));
$securedKey = Crypt::unlockSecuredKey($randomKey, ConfigDB::getValue('tempmaster_passkey'));
return Crypt::getDecrypt($passLogin, $passLoginIV, $pass);
return Crypt::decrypt(ConfigDB::getValue('tempmaster_pass'), $securedKey);
}
}

View File

@@ -258,6 +258,10 @@ class Init
*/
private static function loadExtensions()
{
$PluginsLoader = new \SplClassLoader('Defuse\Crypto', EXTENSIONS_PATH . DIRECTORY_SEPARATOR . 'php-encryption');
$PluginsLoader->setPrepend(false);
$PluginsLoader->register();
$PhpSecLoader = new \SplClassLoader('phpseclib', EXTENSIONS_PATH);
$PhpSecLoader->setPrepend(false);
$PhpSecLoader->register();
@@ -681,6 +685,9 @@ class Init
/**
* Inicialiar la sesión de usuario
*
* @throws \Defuse\Crypto\Exception\BadFormatException
* @throws \Defuse\Crypto\Exception\EnvironmentIsBrokenException
*/
private static function initSession()
{
@@ -705,13 +712,16 @@ class Init
Session::setSidStartTime(time());
Session::setStartActivity(time());
} else if (Session::getUserData()->getUserId() > 0 && time() - Session::getSidStartTime() > $sessionLifeTime / 2) {
$sessionMPass = SessionUtil::getSessionMPass();
$sessionMPass = Crypt\Session::getSessionKey();
session_regenerate_id(true);
// Regenerar la clave maestra
Crypt\Session::saveSessionKey($sessionMPass);
Session::setSidStartTime(time());
// Recargar los permisos del perfil de usuario
Session::setUserProfile(Profile::getItem()->getById(Session::getUserData()->getUserProfileId()));
// Regenerar la clave maestra
SessionUtil::saveSessionMPass($sessionMPass);
}
Session::setLastActivity(time());

View File

@@ -30,6 +30,7 @@ use PDOException;
use SP\Config\Config;
use SP\Config\ConfigData;
use SP\Config\ConfigDB;
use SP\Core\Crypt\Hash;
use SP\Core\Exceptions\InvalidArgumentException;
use SP\Core\Exceptions\SPException;
use SP\DataModel\GroupData;
@@ -490,7 +491,7 @@ class Installer
}
// Guardar el hash de la clave maestra
ConfigDB::setCacheConfigValue('masterPwd', Crypt::mkHashPassword($this->InstallData->getMasterPassword()));
ConfigDB::setCacheConfigValue('masterPwd', Hash::hashKey($this->InstallData->getMasterPassword()));
ConfigDB::setCacheConfigValue('lastupdatempass', time());
ConfigDB::writeConfig(true);

View File

@@ -36,53 +36,10 @@ defined('APP_ROOT') || die();
/**
* Esta clase es la encargada de realizar el encriptado/desencriptado de claves
*/
class Crypt
class OldCrypt
{
public static $strInitialVector;
/**
* Comprobar el hash de una clave.
*
* @param string $pwd con la clave a comprobar
* @param string $checkedHash con el hash a comprobar
* @param bool $isMPass si es la clave maestra
* @return bool
* @throws \SP\Core\Exceptions\SPException
*/
public static function checkHashPass($pwd, $checkedHash, $isMPass = false)
{
if ($isMPass) {
// Comprobar si el hash está en formato anterior a 12002
if (strlen($checkedHash) === 128) {
$check = (hash('sha256', substr($checkedHash, 0, 64) . $pwd) === substr($checkedHash, 64, 64));
if ($check) {
$newHash = self::mkHashPassword($pwd);
AccountHistory::updateAccountsMPassHash($newHash);
ConfigDB::setValue('masterPwd', $newHash);
Log::writeNewLog(__('Aviso', false), __('Se ha regenerado el HASH de clave maestra. No es necesaria ninguna acción.', false), Log::NOTICE);
}
return $check;
// Hash de clave maestra anterior a 2.0.0.17013101
} elseif (hash_equals(crypt($pwd, substr($checkedHash, 0, 72)), substr($checkedHash, 72))) {
ConfigDB::setValue('masterPwd', Crypt::mkHashPassword($pwd));
Log::writeNewLog(__('Aviso', false), __('Se ha regenerado el HASH de clave maestra. No es necesaria ninguna acción.', false), Log::NOTICE);
return true;
}
}
// Timing attacks...
// usleep(mt_rand(100000, 300000));
// Obtener el hash de la clave y la clave para generar una clave y comparar
return hash_equals(crypt($pwd, substr($checkedHash, 0, 30)), substr($checkedHash, 30));
}
/**
* Generar un hash de una clave utilizando un salt.
*
@@ -206,7 +163,7 @@ class Crypt
}
// Comprobar el módulo de encriptación
if (!Crypt::checkCryptModule()) {
if (!OldCrypt::checkCryptModule()) {
throw new SPException(
SPException::SP_CRITICAL,
__('Error interno', false),
@@ -214,8 +171,9 @@ class Crypt
);
}
// FIXME
// Encriptar datos
$encData['data'] = Crypt::mkEncrypt($data, $pwd);
$encData['data'] = OldCrypt::mkEncrypt($data, $pwd);
if (!empty($data) && ($encData['data'] === false || null === $encData['data'])) {
throw new SPException(
@@ -225,7 +183,7 @@ class Crypt
);
}
$encData['iv'] = Crypt::$strInitialVector;
$encData['iv'] = OldCrypt::$strInitialVector;
return $encData;
}

View File

@@ -27,6 +27,7 @@ namespace SP\Core;
use SP\Config\Config;
use SP\DataModel\UserData;
use SP\Mgmt\Profiles\Profile;
use SP\Core\Crypt\Session as SessionCrypt;
defined('APP_ROOT') || die();
@@ -60,27 +61,16 @@ class SessionUtil
Session::setPublicKey($CryptPKI->getPublicKey());
}
/**
* Guardar la clave maestra encriptada en la sesión
*
* @param $masterPass
*/
public static function saveSessionMPass($masterPass)
{
$sessionMasterPass = Crypt::mkCustomMPassEncrypt(Crypt::generateAesKey(session_id()), $masterPass);
Session::setMPass($sessionMasterPass[0]);
Session::setMPassIV($sessionMasterPass[1]);
}
/**
* Desencriptar la clave maestra de la sesión.
*
* @return string con la clave maestra
* @throws \Defuse\Crypto\Exception\EnvironmentIsBrokenException
* @throws \Defuse\Crypto\Exception\BadFormatException
*/
public static function getSessionMPass()
{
return Crypt::getDecrypt(Session::getMPass(), Session::getMPassIV(), Crypt::generateAesKey(session_id()));
return SessionCrypt::getSessionKey();
}
/**

View File

@@ -0,0 +1,130 @@
<?php
/**
* sysPass
*
* @author nuxsmin
* @link http://syspass.org
* @copyright 2012-2017, Rubén Domínguez nuxsmin@$syspass.org
*
* This file is part of sysPass.
*
* sysPass is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* sysPass is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with sysPass. If not, see <http://www.gnu.org/licenses/>.
*/
namespace SP\Core\Upgrade;
use Defuse\Crypto\Exception\CryptoException;
use SP\Account\AccountCrypt;
use SP\Account\AccountHistory;
use SP\Account\AccountHistoryCrypt;
use SP\Config\ConfigDB;
use SP\Core\Crypt\Hash;
use SP\Core\Exceptions\SPException;
use SP\Log\Log;
use SP\Mgmt\CustomFields\CustomFieldsUtil;
/**
* Class Crypt
*
* @package SP\Core\Upgrade
*/
class Crypt
{
/**
* Migrar elementos encriptados
*
* @param $masterPass
* @return bool
*/
public static function migrate(&$masterPass)
{
try {
self::migrateAccounts($masterPass);
self::migrateCustomFields($masterPass);
} catch (CryptoException $e) {
return false;
} catch (SPException $e) {
return false;
}
return true;
}
/**
* Migrar claves de cuentas a nuevo formato
*
* @param $masterPass
* @throws \Defuse\Crypto\Exception\BadFormatException
* @throws \Defuse\Crypto\Exception\EnvironmentIsBrokenException
*/
private static function migrateAccounts(&$masterPass)
{
$AccountCrypt = new AccountCrypt();
$AccountCrypt->updateOldPass($masterPass);
$AccountHistoryCrypt = new AccountHistoryCrypt();
$AccountHistoryCrypt->updateOldPass($masterPass);
}
/**
* Migrar los datos de los campos personalizados a nuevo formato
*
* @param $masterPass
* @throws \Defuse\Crypto\Exception\BadFormatException
* @throws \Defuse\Crypto\Exception\EnvironmentIsBrokenException
* @throws \SP\Core\Exceptions\SPException
*/
private static function migrateCustomFields(&$masterPass)
{
CustomFieldsUtil::updateCustomFieldsOldCrypt($masterPass);
}
/**
* Migrar el hash de clave maestra
*
* @param $masterPass
* @return bool
* @throws \SP\Core\Exceptions\QueryException
* @throws \SP\Core\Exceptions\ConstraintException
*/
public static function migrateHash(&$masterPass)
{
$configHashMPass = ConfigDB::getValue('masterPwd');
// Comprobar si el hash está en formato anterior a 12002
if (strlen($configHashMPass) === 128) {
if (hash('sha256', substr($configHashMPass, 0, 64) . $masterPass) === substr($configHashMPass, 64, 64)) {
$newHash = Hash::hashKey($masterPass);
AccountHistory::updateAccountsMPassHash($newHash);
ConfigDB::setValue('masterPwd', $newHash);
Log::writeNewLog(__('Aviso', false), __('Se ha regenerado el HASH de clave maestra. No es necesaria ninguna acción.', false), Log::NOTICE);
return true;
}
// Hash de clave maestra anterior a 2.0.0.17013101
} elseif (hash_equals(crypt($masterPass, substr($configHashMPass, 0, 72)), substr($configHashMPass, 72))
|| hash_equals(crypt($masterPass, substr($configHashMPass, 0, 30)), substr($configHashMPass, 30))
) {
ConfigDB::setValue('masterPwd', Hash::hashKey($masterPass));
Log::writeNewLog(__('Aviso', false), __('Se ha regenerado el HASH de clave maestra. No es necesaria ninguna acción.', false), Log::NOTICE);
return true;
}
return false;
}
}

View File

@@ -26,6 +26,8 @@ namespace SP\Core;
use SP\Account\AccountUtil;
use SP\Config\Config;
use SP\Core\Crypt\Crypt;
use SP\Core\Crypt\Hash;
use SP\Core\Exceptions\SPException;
use SP\DataModel\CategoryData;
use SP\Log\Email;
@@ -270,22 +272,22 @@ class XmlExport
$nodeXML = $this->xml->saveXML($node);
// Crear los datos encriptados con la información del nodo
$encrypted = Crypt::mkEncrypt($nodeXML, $this->exportPass);
$encryptedIV = Crypt::$strInitialVector;
$securedKey = Crypt::makeSecuredKey($this->exportPass);
$encrypted = Crypt::encrypt($nodeXML, $securedKey);
// Buscar si existe ya un nodo para el conjunto de datos encriptados
$encryptedNode = $this->root->getElementsByTagName('Encrypted')->item(0);
if (!$encryptedNode instanceof \DOMElement) {
$encryptedNode = $this->xml->createElement('Encrypted');
$encryptedNode->setAttribute('hash', Crypt::mkHashPassword($this->exportPass));
$encryptedNode->setAttribute('hash', Hash::hashKey($this->exportPass));
}
// Crear el nodo hijo con los datos encriptados
$encryptedData = $this->xml->createElement('Data', base64_encode($encrypted));
$encryptedDataIV = $this->xml->createAttribute('iv');
$encryptedDataIV->value = base64_encode($encryptedIV);
$encryptedDataIV = $this->xml->createAttribute('key');
$encryptedDataIV->value = $securedKey;
// Añadir nodos de datos
$encryptedData->appendChild($encryptedDataIV);

View File

@@ -25,7 +25,7 @@
namespace SP\Import;
use SP\Account\Account;
use SP\Core\Crypt;
use SP\Core\OldCrypt;
use SP\Core\Exceptions\SPException;
use SP\Core\Messages\LogMessage;
use SP\DataModel\AccountExtData;
@@ -126,7 +126,11 @@ abstract class ImportBase implements ImportInterface
}
if ($this->ImportParams->getImportMasterPwd() !== '') {
$pass = Crypt::getDecrypt($AccountData->getAccountPass(), $AccountData->getAccountIV(), $this->ImportParams->getImportMasterPwd());
$securedKey = Crypt\Crypt::unlockSecuredKey($AccountData->getAccountIV(), $this->ImportParams->getImportMasterPwd());
$pass = Crypt\Crypt::decrypt($AccountData->getAccountPass(), $securedKey);
// TODO: importar con encriptación anterior
// $pass = Crypt::getDecrypt($AccountData->getAccountPass(), $AccountData->getAccountIV(), $this->ImportParams->getImportMasterPwd());
$AccountData->setAccountPass($pass);
}

View File

@@ -2,8 +2,8 @@
/**
* sysPass
*
* @author nuxsmin
* @link http://syspass.org
* @author nuxsmin
* @link http://syspass.org
* @copyright 2012-2017, Rubén Domínguez nuxsmin@$syspass.org
*
* This file is part of sysPass.
@@ -24,7 +24,8 @@
namespace SP\Import;
use SP\Core\Crypt;
use SP\Core\OldCrypt;
use SP\Core\Crypt\Hash;
use SP\Core\Exceptions\SPException;
use SP\DataModel\AccountExtData;
use SP\DataModel\CategoryData;
@@ -105,16 +106,20 @@ class SyspassImport extends ImportBase
{
$hash = $this->xmlDOM->getElementsByTagName('Encrypted')->item(0)->getAttribute('hash');
if ($hash !== '' && !Crypt::checkHashPass($this->ImportParams->getImportPwd(), $hash)) {
if ($hash !== '' && !Hash::checkHashKey($this->ImportParams->getImportPwd(), $hash)) {
throw new SPException(SPException::SP_ERROR, __('Clave de encriptación incorrecta', false));
}
foreach ($this->xmlDOM->getElementsByTagName('Data') as $node) {
/** @var $node \DOMElement */
$data = base64_decode($node->nodeValue);
$iv = base64_decode($node->getAttribute('iv'));
$xmlDecrypted = Crypt::getDecrypt($data, $iv, $this->ImportParams->getImportPwd());
if ($iv = base64_decode($node->getAttribute('iv'))) {
$xmlDecrypted = OldCrypt::getDecrypt($data, $iv, $this->ImportParams->getImportPwd());
} else {
$securedKey = Crypt\Crypt::unlockSecuredKey($node->getAttribute('key'), $this->ImportParams->getImportPwd());
$xmlDecrypted = Crypt\Crypt::decrypt($data, $securedKey);
}
$newXmlData = new \DOMDocument();
// $newXmlData->preserveWhiteSpace = true;

View File

@@ -26,7 +26,7 @@ namespace SP\Mgmt\CustomFields;
defined('APP_ROOT') || die();
use SP\Core\Crypt;
use SP\Core\OldCrypt;
use SP\DataModel\CustomFieldData;
use SP\DataModel\CustomFieldDefData;
use SP\Mgmt\ItemInterface;
@@ -79,7 +79,7 @@ class CustomField extends CustomFieldBase implements ItemInterface
return $this->delete($this->itemData->getId());
}
$cryptData = Crypt::encryptData($this->itemData->getValue());
$securedKey = Crypt\Crypt::makeSecuredKey(Crypt\Session::getSessionKey());
$query = /** @lang SQL */
'UPDATE customFieldsData SET
@@ -91,8 +91,8 @@ class CustomField extends CustomFieldBase implements ItemInterface
$Data = new QueryData();
$Data->setQuery($query);
$Data->addParam($cryptData['data']);
$Data->addParam($cryptData['iv']);
$Data->addParam(Crypt\Crypt::encrypt($this->itemData->getValue(), $securedKey));
$Data->addParam($securedKey);
$Data->addParam($this->itemData->getModule());
$Data->addParam($this->itemData->getId());
$Data->addParam($this->itemData->getDefinitionId());
@@ -136,7 +136,7 @@ class CustomField extends CustomFieldBase implements ItemInterface
return true;
}
$cryptData = Crypt::encryptData($this->itemData->getValue());
$securedKey = Crypt\Crypt::makeSecuredKey(Crypt\Session::getSessionKey());
$query = /** @lang SQL */
'INSERT INTO customFieldsData SET
@@ -151,8 +151,8 @@ class CustomField extends CustomFieldBase implements ItemInterface
$Data->addParam($this->itemData->getId());
$Data->addParam($this->itemData->getModule());
$Data->addParam($this->itemData->getDefinitionId());
$Data->addParam($cryptData['data']);
$Data->addParam($cryptData['iv']);
$Data->addParam(Crypt\Crypt::encrypt($this->itemData->getValue(), $securedKey));
$Data->addParam($securedKey);
return DB::getQuery($Data);
}
@@ -248,7 +248,9 @@ class CustomField extends CustomFieldBase implements ItemInterface
protected function unencryptData(CustomFieldData $CustomFieldData)
{
if ($CustomFieldData->getCustomfielddataData() !== '') {
return $this->formatValue(Crypt::getDecrypt($CustomFieldData->getCustomfielddataData(), $CustomFieldData->getCustomfielddataIv()));
$securedKey = Crypt\Crypt::unlockSecuredKey($CustomFieldData->getCustomfielddataIv(), Crypt\Session::getSessionKey());
return $this->formatValue(Crypt\Crypt::decrypt($CustomFieldData->getCustomfielddataData(), $securedKey));
}
return '';

View File

@@ -26,7 +26,7 @@ namespace SP\Mgmt\CustomFields;
defined('APP_ROOT') || die();
use SP\Core\Crypt;
use SP\Core\OldCrypt;
use SP\Core\Exceptions\SPException;
use SP\DataModel\CustomFieldData;
use SP\DataModel\CustomFieldDefData;
@@ -60,6 +60,8 @@ class CustomFieldsUtil
* @param string $currentMasterPass La clave maestra actual
* @param string $newMasterPassword La nueva clave maestra
* @return bool
* @throws \Defuse\Crypto\Exception\BadFormatException
* @throws \Defuse\Crypto\Exception\EnvironmentIsBrokenException
* @throws \SP\Core\Exceptions\SPException
*/
public static function updateCustomFieldsCrypt($currentMasterPass, $newMasterPassword)
@@ -90,8 +92,10 @@ class CustomFieldsUtil
$success = [];
foreach ($queryRes as $CustomField) {
$fieldData = Crypt::getDecrypt($CustomField->getCustomfielddataData(), $CustomField->getCustomfielddataIv(), $currentMasterPass);
$fieldCryptData = Crypt::encryptData($fieldData, $newMasterPassword);
$currentSecuredKey = Crypt\Crypt::unlockSecuredKey($CustomField->getCustomfielddataIv(), $currentMasterPass);
$fieldData = Crypt\Crypt::decrypt($CustomField->getCustomfielddataData(), $currentSecuredKey);
$securedKey = Crypt\Crypt::makeSecuredKey($newMasterPassword);
$query = /** @lang SQL */
'UPDATE customFieldsData SET
@@ -101,8 +105,76 @@ class CustomFieldsUtil
$Data = new QueryData();
$Data->setQuery($query);
$Data->addParam($fieldCryptData['data']);
$Data->addParam($fieldCryptData['iv']);
$Data->addParam(Crypt\Crypt::encrypt($fieldData, $securedKey));
$Data->addParam($securedKey);
$Data->addParam($CustomField->getCustomfielddataId());
try {
DB::getQuery($Data);
$success[] = $CustomField->getCustomfielddataId();
} catch (SPException $e) {
$errors[] = $CustomField->getCustomfielddataId();
}
}
$LogMessage->addDetails(__('Registros no actualizados', false), implode(',', $errors));
$LogMessage->addDetails(__('Registros actualizados', false), implode(',', $success));
$Log->writeLog();
return (count($errors) === 0);
}
/**
* Actualizar los datos encriptados con una nueva clave
*
* @param string $currentMasterPass La clave maestra actual
* @return bool
* @throws \Defuse\Crypto\Exception\EnvironmentIsBrokenException
* @throws \Defuse\Crypto\Exception\BadFormatException
* @throws \SP\Core\Exceptions\SPException
*/
public static function updateCustomFieldsOldCrypt(&$currentMasterPass)
{
$Log = new Log();
$LogMessage = $Log->getLogMessage();
$LogMessage->setAction(__('Campos Personalizados', false));
$query = /** @lang SQL */
'SELECT customfielddata_id, customfielddata_data, customfielddata_iv FROM customFieldsData';
$Data = new QueryData();
$Data->setMapClassName(CustomFieldData::class);
$Data->setQuery($query);
/** @var CustomFieldData[] $queryRes */
$queryRes = DB::getResultsArray($Data);
if (count($queryRes) === 0) {
$LogMessage->addDescription(__('No hay datos de campos personalizados', false));
$Log->writeLog();
return true;
}
$LogMessage->addDescription(__('Actualizando datos encriptados', false));
$errors = [];
$success = [];
foreach ($queryRes as $CustomField) {
$securedKey = Crypt\Crypt::makeSecuredKey($currentMasterPass);
$fieldData = OldCrypt::getDecrypt($CustomField->getCustomfielddataData(), $CustomField->getCustomfielddataIv(), $currentMasterPass);
$query = /** @lang SQL */
'UPDATE customFieldsData SET
customfielddata_data = ?,
customfielddata_iv = ?
WHERE customfielddata_id = ?';
$Data = new QueryData();
$Data->setQuery($query);
$Data->addParam(Crypt\Crypt::encrypt($fieldData, $securedKey));
$Data->addParam($securedKey);
$Data->addParam($CustomField->getCustomfielddataId());
try {

View File

@@ -27,12 +27,13 @@ namespace SP\Mgmt\PublicLinks;
defined('APP_ROOT') || die();
use SP\Config\Config;
use SP\Core\Crypt;
use SP\Core\OldCrypt;
use SP\Core\Exceptions\SPException;
use SP\Core\SessionUtil;
use SP\DataModel\PublicLinkData;
use SP\Mgmt\ItemBase;
use SP\DataModel\PublicLinkBaseData;
use SP\Util\Util;
/**
* Class PublicLinks para la gestión de enlaces públicos
@@ -71,14 +72,15 @@ abstract class PublicLinkBase extends ItemBase
* Devolver la clave y el IV para el enlace
*
* @throws SPException
* @throws \Defuse\Crypto\Exception\BadFormatException
* @throws \Defuse\Crypto\Exception\EnvironmentIsBrokenException
*/
protected final function createLinkPass()
{
$pass = Crypt::generateAesKey($this->createLinkHash());
$cryptPass = Crypt::encryptData(SessionUtil::getSessionMPass(), $pass);
$securedKey = Crypt\Crypt::makeSecuredKey(Config::getConfig()->getPasswordSalt() . $this->createLinkHash());
$this->itemData->setPass($cryptPass['data']);
$this->itemData->setPassIV($cryptPass['iv']);
$this->itemData->setPass(Crypt\Crypt::encrypt(Crypt\Session::getSessionKey(), $securedKey));
$this->itemData->setPassIV($securedKey);
}
/**

View File

@@ -27,7 +27,8 @@ namespace SP\Mgmt\Users;
defined('APP_ROOT') || die();
use SP\Config\ConfigDB;
use SP\Core\Crypt;
use SP\Core\OldCrypt;
use SP\Core\Crypt\Hash;
use SP\Core\Exceptions\SPException;
use SP\Core\SessionUtil;
use SP\DataModel\UserPassData;
@@ -165,15 +166,16 @@ class UserPass extends UserBase
*/
public static function makeUserPassHash($userPass)
{
$salt = Crypt::makeHashSalt();
return ['salt' => $salt, 'pass' => crypt($userPass, $salt)];
return ['salt' => '', 'pass' => Hash::hashKey($userPass)];
}
/**
* Comprueba la clave maestra del usuario.
*
* @return bool
*
* @throws \Defuse\Crypto\Exception\EnvironmentIsBrokenException
* @throws \Defuse\Crypto\Exception\BadFormatException
* @throws \SP\Core\Exceptions\SPException
*/
public function loadUserMPass()
@@ -184,11 +186,11 @@ class UserPass extends UserBase
if ($userMPass === false || empty($configHashMPass)) {
return false;
// Comprobamos el hash de la clave del usuario con la guardada
} elseif (Crypt::checkHashPass($userMPass, $configHashMPass, true)) {
// Comprobamos el hash de la clave del usuario con la guardada
} elseif (Hash::checkHashKey($userMPass, $configHashMPass)) {
$this->clearUserMPass = $userMPass;
SessionUtil::saveSessionMPass($userMPass);
Crypt\Session::saveSessionKey($userMPass);
return true;
}
@@ -224,7 +226,9 @@ class UserPass extends UserBase
$this->itemData->setUserMPass($queryRes->user_mPass);
$this->itemData->setUserMIV($queryRes->user_mIV);
return Crypt::getDecrypt($queryRes->user_mPass, $queryRes->user_mIV, $this->getCypherPass($cypher));
$securedKey = Crypt\Crypt::unlockSecuredKey($queryRes->user_mIV, $this->getCypherPass($cypher));
return Crypt\Crypt::decrypt($queryRes->user_mPass, $securedKey);
}
/**
@@ -237,7 +241,7 @@ class UserPass extends UserBase
{
$pass = $cypher === null ? $this->itemData->getUserPass() : $cypher;
return Crypt::generateAesKey($pass . $this->itemData->getUserLogin());
return Crypt\Crypt::makeSecuredKey($pass . $this->itemData->getUserLogin());
}
/**
@@ -280,12 +284,15 @@ class UserPass extends UserBase
if ($configHashMPass === false) {
return false;
} elseif (null === $configHashMPass) {
$configHashMPass = Crypt::mkHashPassword($masterPwd);
$configHashMPass = Hash::hashKey($masterPwd);
ConfigDB::setValue('masterPwd', $configHashMPass);
}
if (Crypt::checkHashPass($masterPwd, $configHashMPass, true)) {
$cryptMPass = Crypt::mkCustomMPassEncrypt($this->getCypherPass(), $masterPwd);
if (Hash::checkHashKey($masterPwd, $configHashMPass)
|| \SP\Core\Upgrade\Crypt::migrateHash($masterPwd)
) {
$securedKey = Crypt\Crypt::makeSecuredKey($this->getCypherPass());
$cryptMPass = Crypt\Crypt::encrypt($masterPwd, $securedKey);
if (!empty($cryptMPass)) {
$query = /** @lang SQL */
@@ -297,14 +304,14 @@ class UserPass extends UserBase
$Data = new QueryData();
$Data->setQuery($query);
$Data->addParam($cryptMPass[0]);
$Data->addParam($cryptMPass[1]);
$Data->addParam($cryptMPass);
$Data->addParam($securedKey);
$Data->addParam($this->itemData->getUserId());
$this->clearUserMPass = $masterPwd;
$this->itemData->setUserMPass($cryptMPass[0]);
$this->itemData->setUserMIV($cryptMPass[1]);
$this->itemData->setUserMPass($cryptMPass);
$this->itemData->setUserMIV($securedKey);
DB::getQuery($Data);

View File

@@ -121,9 +121,13 @@ class Util
*/
public static function generateRandomBytes($length = 30)
{
if (function_exists('random_bytes')) {
return bin2hex(random_bytes($length));
}
// Try to use openssl_random_pseudo_bytes
if (function_exists('openssl_random_pseudo_bytes')) {
$pseudo_byte = bin2hex(openssl_random_pseudo_bytes($length));
$pseudo_byte = bin2hex(openssl_random_pseudo_bytes($length, $strong));
return substr($pseudo_byte, 0, $length); // Truncate it to match the length
}