LICENSE 0000666 00000040525 13536431632 0005571 0 ustar 00 Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.
stub/sodium_stub.php 0000666 00000066610 13536431632 0010612 0 ustar 00
README.md 0000666 00000022056 13536431632 0006042 0 ustar 00 # Halite
[![Build Status](https://travis-ci.org/paragonie/halite.svg?branch=master)](https://travis-ci.org/paragonie/halite)
[![Latest Stable Version](https://poser.pugx.org/paragonie/halite/v/stable)](https://packagist.org/packages/paragonie/halite)
[![Latest Unstable Version](https://poser.pugx.org/paragonie/halite/v/unstable)](https://packagist.org/packages/paragonie/halite)
[![License](https://poser.pugx.org/paragonie/halite/license)](https://packagist.org/packages/paragonie/halite)
[![Downloads](https://img.shields.io/packagist/dt/paragonie/halite.svg)](https://packagist.org/packages/paragonie/halite)
[![Coverage Status](https://coveralls.io/repos/github/paragonie/halite/badge.svg?branch=master)](https://coveralls.io/github/paragonie/halite?branch=master)
**Halite** is a high-level cryptography interface that relies on [libsodium](https://pecl.php.net/package/libsodium)
for all of its underlying cryptography operations.
Halite was created by [Paragon Initiative Enterprises](https://paragonie.com) as
a result of our continued efforts to improve the ecosystem and make [cryptography in PHP](https://paragonie.com/blog/2015/09/state-cryptography-in-php)
safer and easier to implement.
> You can read the [**Halite Documentation**](https://github.com/paragonie/halite/tree/master/doc) online.
Halite is released under Mozilla Public License 2.0. [Commercial licenses are available](https://paragonie.com/contact)
from Paragon Initiative Enterprises if you wish to extend Halite without making your
derivative works available under the terms of the MPL.
If you are satisfied with the terms of MPL software for backend web applications
but would like to purchase a support contract for your application that uses Halite,
those are also offered by Paragon Initiative Enterprises.
**Important:** Earlier versions of Halite were available under the GNU Public License
version 3 (GPLv3). Only Halite 4.0.1 and newer are available under the Mozilla Public
License terms.
## Installing Halite
Before you can use Halite, you must choose a version that fits the requirements
of your project. The differences between the requirements for the available
versions of Halite are briefly highlighted below.
| | PHP | libsodium | PECL libsodium | Support |
|-------------------------------------------------------------|-------|-----------|----------------|---------------------------|
| Halite 4.1 and newer | 7.2.0 | 1.0.15 | N/A (standard) | :heavy_check_mark: Active |
| [Halite 4.0](https://github.com/paragonie/halite/tree/v4.0) | 7.2.0 | 1.0.13 | N/A (standard) | :heavy_check_mark: Active |
| [Halite 3](https://github.com/paragonie/halite/tree/v3.x) | 7.0.0 | 1.0.9 | 1.0.6 / 2.0.4 | :x: Not Supported |
| [Halite 2](https://github.com/paragonie/halite/tree/v2.2) | 7.0.0 | 1.0.9 | 1.0.6 | :x: Not Supported |
| [Halite 1](https://github.com/paragonie/halite/tree/v1.x) | 5.6.0 | 1.0.6 | 1.0.2 | :x: Not Supported |
If you need a version of Halite before 4.0, see the documentation relevant to that
particular branch.
**To install Halite, you first need to [install libsodium](https://paragonie.com/book/pecl-libsodium/read/00-intro.md#installing-libsodium).**
You may or may not need the PHP extension. For most people, this means running...
sudo apt-get install php7.2-sodium
...or an equivalent command for your operating system and PHP version.
If you're stuck, [this step-by-step guide contributed by @aolko](doc/Install-Guides/Ubuntu.md) may be helpful.
Once you have the prerequisites installed, install Halite through [Composer](https://getcomposer.org/doc/00-intro.md):
composer require paragonie/halite:^4
### Commercial Support for Older Halite Versions
Free (gratis) support for Halite only extends to the most recent major version (currently 4).
If your company requires support for an older version of Halite,
[contact Paragon Initiative Enterprises](https://paragonie.com/contact) to inquire about
commercial support options.
If you need an easy way to migrate from older versions of Halite, check out [halite-legacy](https://github.com/paragonie/halite-legacy).
## Using Halite in Your Project
Check out the [documentation](doc). The basic Halite API is designed for simplicity:
* Encryption
* Symmetric
* `Symmetric\Crypto::encrypt`([`HiddenString`](doc/Classes/HiddenString.md), [`EncryptionKey`](doc/Classes/Symmetric/EncryptionKey.md)): `string`
* `Symmetric\Crypto::decrypt`(`string`, [`EncryptionKey`](doc/Classes/Symmetric/EncryptionKey.md)): [`HiddenString`](doc/Classes/HiddenString.md)
* Asymmetric
* Anonymous
* `Asymmetric\Crypto::seal`([`HiddenString`](doc/Classes/HiddenString.md), [`EncryptionPublicKey`](doc/Classes/Asymmetric/EncryptionPublicKey.md)): `string`
* `Asymmetric\Crypto::unseal`(`string`, [`EncryptionSecretKey`](doc/Classes/Asymmetric/EncryptionSecretKey.md)): [`HiddenString`](doc/Classes/HiddenString.md)
* Authenticated
* `Asymmetric\Crypto::encrypt`([`HiddenString`](doc/Classes/HiddenString.md), [`EncryptionSecretKey`](doc/Classes/Asymmetric/EncryptionSecretKey.md), [`EncryptionPublicKey`](doc/Classes/Asymmetric/EncryptionPublicKey.md)): `string`
* `Asymmetric\Crypto::decrypt`(`string`, [`EncryptionSecretKey`](doc/Classes/Asymmetric/EncryptionSecretKey.md), [`EncryptionPublicKey`](doc/Classes/Asymmetric/EncryptionPublicKey.md)): [`HiddenString`](doc/Classes/HiddenString.md)
* Authentication
* Symmetric
* `Symmetric\Crypto::authenticate`(`string`, [`AuthenticationKey`](doc/Classes/Symmetric/AuthenticationKey.md)): `string`
* `Symmetric\Crypto::verify`(`string`, [`AuthenticationKey`](doc/Classes/Symmetric/AuthenticationKey.md), `string`): `bool`
* Asymmetric
* `Asymmetric\Crypto::sign`(`string`, [`SignatureSecretKey`](doc/Classes/Asymmetric/SignatureSecretKey.md)): `string`
* `Asymmetric\Crypto::verify`(`string`, [`SignaturePublicKey`](doc/Classes/Asymmetric/SignaturePublicKey.md), `string`): `bool`
### Example: Encrypting and Decrypting a message
First, generate and persist a key exactly once:
```php
getString() === $message->getString()); // bool(true)
```
This should produce something similar to:
MUIDAEpQznohvNlQ-ZRk-ZZ59Mmox75D_FgAIrXY2cUfStoeL-GIeAe0m-uaeURQdPsVmc5XxRw3-2x5ZAsZH_es37qqFuLFjUI-XK9uG0s30YTsorWfpHdbnqzhRuUOI09c-cKrfMQkNBNm0dDDwZazjTC48zWikRHSHXg8NXerVDebzng1aufc_S-osI_zQuLbZDODujEnpbPZhMMcm4-SWuyVXcBPdGZolJyT
#### Cryptographic Keys in Halite
> **Important**: Halite works with `Key` objects, not strings.
If you attempt to `echo` a key object, you will get an empty string
rather than its contents. If you attempt to `var_dump()` a key object,
you will just get some facts about the type of key it is.
You must invoke `$obj->getRawKeyMaterial()` explicitly if you want
to inspect a key's raw binary contents. This is not recommended for
most use cases.
### Example: Generating a key from a password
```php
PHP Fatal error: Uncaught SodiumException: This is not implemented, as it is not possible to securely wipe memory from PHP
The solution to this is to make sure libsodium is installed/enabled. See above in this
README for more information.
## Support Contracts
If your company uses this library in their products or services, you may be
interested in [purchasing a support contract from Paragon Initiative Enterprises](https://paragonie.com/enterprise).
autoload.php 0000666 00000002167 13536431632 0007105 0 ustar 00 getString(),
$kdfLimits[0],
$kdfLimits[1]
);
// Now let's encrypt the result
return Crypto::encryptWithAd(
new HiddenString($hashed),
$secretKey,
$additionalData
);
}
/**
* Is this password hash stale?
*
* @param string $stored Encrypted password hash
* @param EncryptionKey $secretKey The master key for all passwords
* @param string $level The security level for this password
* @param string $additionalData Additional authenticated data (if used to encrypt, mandatory)
* @return bool Do we need to regenerate the hash or
* ciphertext?
*
* @throws Alerts\InvalidSignature
* @throws CannotPerformOperation
* @throws InvalidDigestLength
* @throws InvalidMessage
* @throws InvalidType
* @throws \TypeError
*/
public static function needsRehash(
string $stored,
EncryptionKey $secretKey,
string $level = KeyFactory::INTERACTIVE,
string $additionalData = ''
): bool {
$config = self::getConfig($stored);
if (Binary::safeStrlen($stored) < ((int) $config->SHORTEST_CIPHERTEXT_LENGTH * 4 / 3)) {
throw new InvalidMessage('Encrypted password hash is too short.');
}
// First let's decrypt the hash
$hash_str = Crypto::decryptWithAd(
$stored,
$secretKey,
$additionalData,
$config->ENCODING
)->getString();
// Upon successful decryption, verify that we're using Argon2i
if (!\hash_equals(
Binary::safeSubstr($hash_str, 0, 10),
\SODIUM_CRYPTO_PWHASH_STRPREFIX
)) {
return true;
}
// Parse the cost parameters:
switch ($level) {
case KeyFactory::INTERACTIVE:
return !\hash_equals(
'$argon2id$v=19$m=65536,t=2,p=1$',
Binary::safeSubstr($hash_str, 0, 31)
);
case KeyFactory::MODERATE:
return !\hash_equals(
'$argon2id$v=19$m=262144,t=3,p=1$',
Binary::safeSubstr($hash_str, 0, 32)
);
case KeyFactory::SENSITIVE:
return !\hash_equals(
'$argon2id$v=19$m=1048576,t=4,p=1$',
Binary::safeSubstr($hash_str, 0, 33)
);
default:
return true;
}
}
/**
* Get the configuration for this version of halite
*
* @param string $stored A stored password hash
* @return SymmetricConfig
* @throws InvalidMessage
* @throws \TypeError
*/
protected static function getConfig(string $stored): SymmetricConfig
{
$length = Binary::safeStrlen($stored);
// This doesn't even have a header.
if ($length < 8) {
throw new InvalidMessage(
'Encrypted password hash is way too short.'
);
}
if (
\hash_equals(Binary::safeSubstr($stored, 0, 5), Halite::VERSION_PREFIX)
||
\hash_equals(Binary::safeSubstr($stored, 0, 5), Halite::VERSION_OLD_PREFIX)
) {
/** @var string $decoded */
$decoded = Base64UrlSafe::decode($stored);
return SymmetricConfig::getConfig(
$decoded,
'encrypt'
);
}
// @codeCoverageIgnoreStart
$v = Hex::decode(Binary::safeSubstr($stored, 0, 8));
return SymmetricConfig::getConfig($v, 'encrypt');
// @codeCoverageIgnoreEnd
}
/**
* Decrypt then verify a password
*
* @param HiddenString $password The user's password
* @param string $stored The encrypted password hash
* @param EncryptionKey $secretKey The master key for all passwords
* @param string $additionalData Additional authenticated data (needed to decrypt)
* @return bool Is this password valid?
*
* @throws Alerts\InvalidSignature
* @throws CannotPerformOperation
* @throws InvalidDigestLength
* @throws InvalidMessage
* @throws InvalidType
* @throws \TypeError
*/
public static function verify(
HiddenString $password,
string $stored,
EncryptionKey $secretKey,
string $additionalData = ''
): bool {
$config = self::getConfig($stored);
// Base64-urlsafe encoded, so 4/3 the size of raw binary
if (Binary::safeStrlen($stored) < ((int) $config->SHORTEST_CIPHERTEXT_LENGTH * 4/3)) {
throw new InvalidMessage(
'Encrypted password hash is too short.'
);
}
// First let's decrypt the hash
$hash_str = Crypto::decryptWithAd($stored, $secretKey, $additionalData, $config->ENCODING);
// Upon successful decryption, verify the password is correct
return \sodium_crypto_pwhash_str_verify(
$hash_str->getString(),
$password->getString()
);
}
}
src/Cookie.php 0000666 00000010721 13536431632 0007270 0 ustar 00 key = $key;
}
/**
* Hide this from var_dump(), etc.
*
* @return array
*/
public function __debugInfo()
{
return [
'key' => 'private'
];
}
/**
* Store a value in an encrypted cookie
*
* @param string $name
* @return mixed|null (typically an array)
* @throws InvalidDigestLength
* @throws InvalidSignature
* @throws CannotPerformOperation
* @throws InvalidType
* @throws \TypeError
*/
public function fetch(string $name)
{
if (!isset($_COOKIE[$name])) {
return null;
}
try {
/** @var string|array|int|float|bool $stored */
$stored = $_COOKIE[$name];
if (!\is_string($stored)) {
throw new InvalidType('Cookie value is not a string');
}
$config = self::getConfig($stored);
$decrypted = Crypto::decrypt(
$stored,
$this->key,
$config->ENCODING
);
return \json_decode($decrypted->getString(), true);
} catch (InvalidMessage $e) {
return null;
}
}
/**
* Get the configuration for this version of halite
*
* @param string $stored A stored password hash
* @return SymmetricConfig
*
* @throws InvalidMessage
* @throws \TypeError
*/
protected static function getConfig(string $stored): SymmetricConfig
{
$length = Binary::safeStrlen($stored);
// This doesn't even have a header.
if ($length < 8) {
throw new InvalidMessage(
'Encrypted password hash is way too short.'
);
}
if (\hash_equals(Binary::safeSubstr($stored, 0, 5), Halite::VERSION_PREFIX)) {
/** @var string $decoded */
$decoded = Base64UrlSafe::decode($stored);
return SymmetricConfig::getConfig(
$decoded,
'encrypt'
);
}
$v = Hex::decode(Binary::safeSubstr($stored, 0, 8));
return SymmetricConfig::getConfig($v, 'encrypt');
}
/**
* Store a value in an encrypted cookie
*
* @param string $name
* @param mixed $value
* @param int $expire (defaults to 0)
* @param string $path (defaults to '/')
* @param string $domain (defaults to NULL)
* @param bool $secure (defaults to TRUE)
* @param bool $httpOnly (defaults to TRUE)
* @return bool
*
* @throws InvalidDigestLength
* @throws CannotPerformOperation
* @throws InvalidMessage
* @throws InvalidType
* @throws \TypeError
* @psalm-suppress MixedArgument
*/
public function store(
string $name,
$value,
int $expire = 0,
string $path = '/',
string $domain = '',
bool $secure = true,
bool $httpOnly = true
): bool {
return \setcookie(
$name,
Crypto::encrypt(
new HiddenString(
(string) \json_encode($value)
),
$this->key
),
(int) $expire,
(string) $path,
(string) $domain,
(bool) $secure,
(bool) $httpOnly
);
}
}
src/KeyFactory.php 0000666 00000062051 13536431632 0010142 0 ustar 00 getMessage());
}
// @codeCoverageIgnoreEnd
return new AuthenticationKey(
new HiddenString($secretKey)
);
}
/**
* Generate an an encryption key (symmetric-key cryptography)
*
* @return EncryptionKey
* @throws CannotPerformOperation
* @throws InvalidKey
* @throws \TypeError
*/
public static function generateEncryptionKey(): EncryptionKey
{
// @codeCoverageIgnoreStart
try {
$secretKey = \random_bytes(\SODIUM_CRYPTO_STREAM_KEYBYTES);
} catch (\Throwable $ex) {
throw new CannotPerformOperation($ex->getMessage());
}
// @codeCoverageIgnoreEnd
return new EncryptionKey(
new HiddenString($secretKey)
);
}
/**
* Generate a key pair for public key encryption
*
* @return \ParagonIE\Halite\EncryptionKeyPair
*
* @throws InvalidKey
* @throws \TypeError
*/
public static function generateEncryptionKeyPair(): EncryptionKeyPair
{
// Encryption keypair
$kp = \sodium_crypto_box_keypair();
$secretKey = \sodium_crypto_box_secretkey($kp);
// Let's wipe our $kp variable
\sodium_memzero($kp);
return new EncryptionKeyPair(
new EncryptionSecretKey(
new HiddenString($secretKey)
)
);
}
/**
* Generate a key pair for public key digital signatures
*
* @return SignatureKeyPair
* @throws InvalidKey
* @throws \TypeError
*/
public static function generateSignatureKeyPair(): SignatureKeyPair
{
// Encryption keypair
$kp = \sodium_crypto_sign_keypair();
$secretKey = \sodium_crypto_sign_secretkey($kp);
// Let's wipe our $kp variable
\sodium_memzero($kp);
return new SignatureKeyPair(
new SignatureSecretKey(
new HiddenString($secretKey)
)
);
}
/**
* Derive an authentication key (symmetric) from a password and salt
*
* @param HiddenString $password
* @param string $salt
* @param string $level Security level for KDF
* @param int $alg Which Argon2 variant to use?
* (You can safely use the default)
*
* @return AuthenticationKey
*
* @throws InvalidKey
* @throws InvalidSalt
* @throws InvalidType
* @throws \TypeError
*/
public static function deriveAuthenticationKey(
HiddenString $password,
string $salt,
string $level = self::INTERACTIVE,
int $alg = SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13
): AuthenticationKey {
$kdfLimits = self::getSecurityLevels($level, $alg);
// VERSION 2+ (argon2)
if (Binary::safeStrlen($salt) !== \SODIUM_CRYPTO_PWHASH_SALTBYTES) {
// @codeCoverageIgnoreStart
throw new InvalidSalt(
'Expected ' . \SODIUM_CRYPTO_PWHASH_SALTBYTES . ' bytes, got ' . Binary::safeStrlen($salt)
);
// @codeCoverageIgnoreEnd
}
/** @var string $secretKey */
$secretKey = @\sodium_crypto_pwhash(
\SODIUM_CRYPTO_AUTH_KEYBYTES,
$password->getString(),
$salt,
$kdfLimits[0],
$kdfLimits[1],
$alg
);
return new AuthenticationKey(
new HiddenString($secretKey)
);
}
/**
* Derive an encryption key (symmetric-key cryptography) from a password
* and salt
*
* @param HiddenString $password
* @param string $salt
* @param string $level Security level for KDF
* @param int $alg Which Argon2 variant to use?
* (You can safely use the default)
*
* @return EncryptionKey
* @throws InvalidKey
* @throws InvalidSalt
* @throws InvalidType
* @throws \TypeError
*/
public static function deriveEncryptionKey(
HiddenString $password,
string $salt,
string $level = self::INTERACTIVE,
int $alg = SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13
): EncryptionKey {
$kdfLimits = self::getSecurityLevels($level, $alg);
// VERSION 2+ (argon2)
if (Binary::safeStrlen($salt) !== \SODIUM_CRYPTO_PWHASH_SALTBYTES) {
// @codeCoverageIgnoreStart
throw new InvalidSalt(
'Expected ' . \SODIUM_CRYPTO_PWHASH_SALTBYTES . ' bytes, got ' . Binary::safeStrlen($salt)
);
// @codeCoverageIgnoreEnd
}
/** @var string $secretKey */
$secretKey = @\sodium_crypto_pwhash(
\SODIUM_CRYPTO_STREAM_KEYBYTES,
$password->getString(),
$salt,
$kdfLimits[0],
$kdfLimits[1],
$alg
);
return new EncryptionKey(
new HiddenString($secretKey)
);
}
/**
* Derive a key pair for public key encryption from a password and salt
*
* @param HiddenString $password
* @param string $salt
* @param string $level Security level for KDF
* @param int $alg Which Argon2 variant to use?
* (You can safely use the default)
*
* @return EncryptionKeyPair
*
* @throws InvalidKey
* @throws InvalidSalt
* @throws InvalidType
* @throws \TypeError
*/
public static function deriveEncryptionKeyPair(
HiddenString $password,
string $salt,
string $level = self::INTERACTIVE,
int $alg = SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13
): EncryptionKeyPair {
$kdfLimits = self::getSecurityLevels($level, $alg);
// VERSION 2+ (argon2)
if (Binary::safeStrlen($salt) !== \SODIUM_CRYPTO_PWHASH_SALTBYTES) {
// @codeCoverageIgnoreStart
throw new InvalidSalt(
'Expected ' . \SODIUM_CRYPTO_PWHASH_SALTBYTES . ' bytes, got ' . Binary::safeStrlen($salt)
);
// @codeCoverageIgnoreEnd
}
// Diffie Hellman key exchange key pair
/** @var string $seed */
$seed = @\sodium_crypto_pwhash(
\SODIUM_CRYPTO_BOX_SEEDBYTES,
$password->getString(),
$salt,
$kdfLimits[0],
$kdfLimits[1],
$alg
);
$keyPair = \sodium_crypto_box_seed_keypair($seed);
$secretKey = \sodium_crypto_box_secretkey($keyPair);
// Let's wipe our $kp variable
\sodium_memzero($keyPair);
return new EncryptionKeyPair(
new EncryptionSecretKey(
new HiddenString($secretKey)
)
);
}
/**
* Derive a key pair for public key signatures from a password and salt
*
* @param HiddenString $password
* @param string $salt
* @param string $level Security level for KDF
* @param int $alg Which Argon2 variant to use?
* (You can safely use the default)
*
* @return SignatureKeyPair
*
* @throws InvalidKey
* @throws InvalidSalt
* @throws InvalidType
* @throws \TypeError
*/
public static function deriveSignatureKeyPair(
HiddenString $password,
string $salt,
string $level = self::INTERACTIVE,
int $alg = SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13
): SignatureKeyPair {
$kdfLimits = self::getSecurityLevels($level, $alg);
// VERSION 2+ (argon2)
if (Binary::safeStrlen($salt) !== \SODIUM_CRYPTO_PWHASH_SALTBYTES) {
// @codeCoverageIgnoreStart
throw new InvalidSalt(
'Expected ' . \SODIUM_CRYPTO_PWHASH_SALTBYTES . ' bytes, got ' . Binary::safeStrlen($salt)
);
// @codeCoverageIgnoreEnd
}
// Digital signature keypair
/** @var string $seed */
$seed = @\sodium_crypto_pwhash(
\SODIUM_CRYPTO_SIGN_SEEDBYTES,
$password->getString(),
$salt,
$kdfLimits[0],
$kdfLimits[1],
$alg
);
$keyPair = \sodium_crypto_sign_seed_keypair($seed);
$secretKey = \sodium_crypto_sign_secretkey($keyPair);
// Let's wipe our $kp variable
\sodium_memzero($keyPair);
return new SignatureKeyPair(
new SignatureSecretKey(
new HiddenString($secretKey)
)
);
}
/**
* Returns a 2D array [OPSLIMIT, MEMLIMIT] for the appropriate security level.
*
* @param string $level
* @param int $alg
* @return int[]
* @throws InvalidType
* @codeCoverageIgnore
*/
public static function getSecurityLevels(
string $level = self::INTERACTIVE,
int $alg = SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13
): array {
switch ($level) {
case self::INTERACTIVE:
if ($alg === SODIUM_CRYPTO_PWHASH_ALG_ARGON2I13) {
// legacy opslimit and memlimit
return [4, 33554432];
}
return [
\SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
\SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE
];
case self::MODERATE:
if ($alg === SODIUM_CRYPTO_PWHASH_ALG_ARGON2I13) {
// legacy opslimit and memlimit
return [6, 134217728];
}
return [
\SODIUM_CRYPTO_PWHASH_OPSLIMIT_MODERATE,
\SODIUM_CRYPTO_PWHASH_MEMLIMIT_MODERATE
];
case self::SENSITIVE:
if ($alg === SODIUM_CRYPTO_PWHASH_ALG_ARGON2I13) {
// legacy opslimit and memlimit
return [8, 536870912];
}
return [
\SODIUM_CRYPTO_PWHASH_OPSLIMIT_SENSITIVE,
\SODIUM_CRYPTO_PWHASH_MEMLIMIT_SENSITIVE
];
default:
throw new InvalidType(
'Invalid security level for Argon2i'
);
}
}
/**
* Load a symmetric authentication key from a string
*
* @param HiddenString $keyData
* @return AuthenticationKey
*
* @throws InvalidKey
* @throws \TypeError
*/
public static function importAuthenticationKey(HiddenString $keyData): AuthenticationKey
{
return new AuthenticationKey(
new HiddenString(
self::getKeyDataFromString(
Hex::decode($keyData->getString())
)
)
);
}
/**
* Load a symmetric encryption key from a string
*
* @param HiddenString $keyData
* @return EncryptionKey
*
* @throws InvalidKey
* @throws \TypeError
*/
public static function importEncryptionKey(HiddenString $keyData): EncryptionKey
{
return new EncryptionKey(
new HiddenString(
self::getKeyDataFromString(
Hex::decode($keyData->getString())
)
)
);
}
/**
* Load, specifically, an encryption public key from a string
*
* @param HiddenString $keyData
* @return EncryptionPublicKey
*
* @throws InvalidKey
* @throws \TypeError
*/
public static function importEncryptionPublicKey(HiddenString $keyData): EncryptionPublicKey
{
return new EncryptionPublicKey(
new HiddenString(
self::getKeyDataFromString(
Hex::decode($keyData->getString())
)
)
);
}
/**
* Load, specifically, an encryption secret key from a string
*
* @param HiddenString $keyData
* @return EncryptionSecretKey
*
* @throws InvalidKey
* @throws \TypeError
*/
public static function importEncryptionSecretKey(HiddenString $keyData): EncryptionSecretKey
{
return new EncryptionSecretKey(
new HiddenString(
self::getKeyDataFromString(
Hex::decode($keyData->getString())
)
)
);
}
/**
* Load, specifically, a signature public key from a string
*
* @param HiddenString $keyData
* @return SignaturePublicKey
*
* @throws InvalidKey
* @throws \TypeError
*/
public static function importSignaturePublicKey(HiddenString $keyData): SignaturePublicKey
{
return new SignaturePublicKey(
new HiddenString(
self::getKeyDataFromString(
Hex::decode($keyData->getString())
)
)
);
}
/**
* Load, specifically, a signature secret key from a string
*
* @param HiddenString $keyData
* @return SignatureSecretKey
*
* @throws InvalidKey
* @throws \TypeError
*/
public static function importSignatureSecretKey(HiddenString $keyData): SignatureSecretKey
{
return new SignatureSecretKey(
new HiddenString(
self::getKeyDataFromString(
Hex::decode($keyData->getString())
)
)
);
}
/**
* Load an asymmetric encryption key pair from a string
*
* @param HiddenString $keyData
* @return EncryptionKeyPair
*
* @throws InvalidKey
* @throws \TypeError
*/
public static function importEncryptionKeyPair(HiddenString $keyData): EncryptionKeyPair
{
return new EncryptionKeyPair(
new EncryptionSecretKey(
new HiddenString(
self::getKeyDataFromString(
Hex::decode($keyData->getString())
)
)
)
);
}
/**
* Load an asymmetric signature key pair from a string
*
* @param HiddenString $keyData
* @return SignatureKeyPair
*
* @throws InvalidKey
* @throws \TypeError
*/
public static function importSignatureKeyPair(HiddenString $keyData): SignatureKeyPair
{
return new SignatureKeyPair(
new SignatureSecretKey(
new HiddenString(
self::getKeyDataFromString(
Hex::decode($keyData->getString())
)
)
)
);
}
/**
* Load a symmetric authentication key from a file
*
* @param string $filePath
* @return AuthenticationKey
*
* @throws CannotPerformOperation
* @throws InvalidKey
* @throws \TypeError
* @codeCoverageIgnore
*/
public static function loadAuthenticationKey(string $filePath): AuthenticationKey
{
if (!\is_readable($filePath)) {
throw new CannotPerformOperation(
'Cannot read keyfile: '. $filePath
);
}
return new AuthenticationKey(
self::loadKeyFile($filePath)
);
}
/**
* Load a symmetric encryption key from a file
*
* @param string $filePath
* @return EncryptionKey
*
* @throws CannotPerformOperation
* @throws InvalidKey
* @throws \TypeError
* @codeCoverageIgnore
*/
public static function loadEncryptionKey(string $filePath): EncryptionKey
{
if (!\is_readable($filePath)) {
throw new CannotPerformOperation(
'Cannot read keyfile: '. $filePath
);
}
return new EncryptionKey(
self::loadKeyFile($filePath)
);
}
/**
* Load, specifically, an encryption public key from a file
*
* @param string $filePath
* @return EncryptionPublicKey
*
* @throws CannotPerformOperation
* @throws InvalidKey
* @throws \TypeError
* @codeCoverageIgnore
*/
public static function loadEncryptionPublicKey(string $filePath): EncryptionPublicKey
{
if (!\is_readable($filePath)) {
throw new CannotPerformOperation(
'Cannot read keyfile: '. $filePath
);
}
return new EncryptionPublicKey(
self::loadKeyFile($filePath)
);
}
/**
* Load, specifically, an encryption public key from a file
*
* @param string $filePath
* @return EncryptionSecretKey
*
* @throws CannotPerformOperation
* @throws InvalidKey
* @throws \TypeError
* @codeCoverageIgnore
*/
public static function loadEncryptionSecretKey(string $filePath): EncryptionSecretKey
{
if (!\is_readable($filePath)) {
throw new CannotPerformOperation(
'Cannot read keyfile: '. $filePath
);
}
return new EncryptionSecretKey(
self::loadKeyFile($filePath)
);
}
/**
* Load, specifically, a signature public key from a file
*
* @param string $filePath
* @return SignaturePublicKey
*
* @throws CannotPerformOperation
* @throws InvalidKey
* @throws \TypeError
* @codeCoverageIgnore
*/
public static function loadSignaturePublicKey(string $filePath): SignaturePublicKey
{
if (!\is_readable($filePath)) {
throw new CannotPerformOperation(
'Cannot read keyfile: '. $filePath
);
}
return new SignaturePublicKey(
self::loadKeyFile($filePath)
);
}
/**
* Load, specifically, a signature secret key from a file
*
* @param string $filePath
* @return SignatureSecretKey
*
* @throws CannotPerformOperation
* @throws InvalidKey
* @throws \TypeError
* @codeCoverageIgnore
*/
public static function loadSignatureSecretKey(string $filePath): SignatureSecretKey
{
if (!\is_readable($filePath)) {
throw new CannotPerformOperation(
'Cannot read keyfile: '. $filePath
);
}
return new SignatureSecretKey(
self::loadKeyFile($filePath)
);
}
/**
* Load an asymmetric encryption key pair from a file
*
* @param string $filePath
* @return EncryptionKeyPair
*
* @throws CannotPerformOperation
* @throws InvalidKey
* @throws \TypeError
* @codeCoverageIgnore
*/
public static function loadEncryptionKeyPair(string $filePath): EncryptionKeyPair
{
if (!\is_readable($filePath)) {
throw new CannotPerformOperation(
'Cannot read keyfile: '. $filePath
);
}
return new EncryptionKeyPair(
new EncryptionSecretKey(
self::loadKeyFile($filePath)
)
);
}
/**
* Load an asymmetric signature key pair from a file
*
* @param string $filePath
* @return SignatureKeyPair
*
* @throws CannotPerformOperation
* @throws InvalidKey
* @throws \TypeError
* @codeCoverageIgnore
*/
public static function loadSignatureKeyPair(string $filePath): SignatureKeyPair
{
if (!\is_readable($filePath)) {
throw new CannotPerformOperation(
'Cannot read keyfile: '. $filePath
);
}
return new SignatureKeyPair(
new SignatureSecretKey(
self::loadKeyFile($filePath)
)
);
}
/**
* Export a cryptography key to a string (with a checksum)
*
* @param object $key
* @return HiddenString
*
* @throws CannotPerformOperation
* @throws InvalidType
* @throws \TypeError
*/
public static function export($key): HiddenString
{
if ($key instanceof KeyPair) {
return self::export(
$key->getSecretKey()
);
} elseif ($key instanceof Key) {
return new HiddenString(
Hex::encode(
Halite::HALITE_VERSION_KEYS . $key->getRawKeyMaterial() .
\sodium_crypto_generichash(
Halite::HALITE_VERSION_KEYS . $key->getRawKeyMaterial(),
'',
\SODIUM_CRYPTO_GENERICHASH_BYTES_MAX
)
)
);
}
throw new \TypeError('Expected a Key.');
}
/**
* Save a key to a file
*
* @param Key|KeyPair $key
* @param string $filename
* @return bool
* @throws \TypeError
*/
public static function save($key, string $filename = ''): bool
{
if ($key instanceof KeyPair) {
return self::saveKeyFile(
$filename,
$key->getSecretKey()->getRawKeyMaterial()
);
}
return self::saveKeyFile($filename, $key->getRawKeyMaterial());
}
/**
* Read a key from a file, verify its checksum
*
* @param string $filePath
* @return HiddenString
* @throws CannotPerformOperation
* @throws InvalidKey
* @throws \TypeError
*/
protected static function loadKeyFile(string $filePath): HiddenString
{
$fileData = \file_get_contents($filePath);
if ($fileData === false) {
// @codeCoverageIgnoreStart
throw new CannotPerformOperation(
'Cannot load key from file: '. $filePath
);
// @codeCoverageIgnoreEnd
}
$data = Hex::decode($fileData);
\sodium_memzero($fileData);
return new HiddenString(
self::getKeyDataFromString($data)
);
}
/**
* Take a stored key string, get the derived key (after verifying the
* checksum)
*
* @param string $data
* @return string
* @throws InvalidKey
* @throws \TypeError
*/
public static function getKeyDataFromString(string $data): string
{
$versionTag = Binary::safeSubstr($data, 0, Halite::VERSION_TAG_LEN);
$keyData = Binary::safeSubstr(
$data,
Halite::VERSION_TAG_LEN,
-\SODIUM_CRYPTO_GENERICHASH_BYTES_MAX
);
$checksum = Binary::safeSubstr(
$data,
-\SODIUM_CRYPTO_GENERICHASH_BYTES_MAX,
\SODIUM_CRYPTO_GENERICHASH_BYTES_MAX
);
$calc = \sodium_crypto_generichash(
$versionTag . $keyData,
'',
\SODIUM_CRYPTO_GENERICHASH_BYTES_MAX
);
if (!\hash_equals($calc, $checksum)) {
// @codeCoverageIgnoreStart
throw new InvalidKey(
'Checksum validation fail'
);
// @codeCoverageIgnoreEnd
}
\sodium_memzero($data);
\sodium_memzero($versionTag);
\sodium_memzero($calc);
\sodium_memzero($checksum);
return $keyData;
}
/**
* Save a key to a file
*
* @param string $filePath
* @param string $keyData
* @return bool
*
* @throws \TypeError
*/
protected static function saveKeyFile(
string $filePath,
string $keyData
): bool {
$saved = \file_put_contents(
$filePath,
Hex::encode(
Halite::HALITE_VERSION_KEYS . $keyData .
\sodium_crypto_generichash(
Halite::HALITE_VERSION_KEYS . $keyData,
'',
\SODIUM_CRYPTO_GENERICHASH_BYTES_MAX
)
)
);
return $saved !== false;
}
}
src/Util.php 0000666 00000021542 13536431632 0006777 0 ustar 00 (255 * \SODIUM_CRYPTO_GENERICHASH_KEYBYTES)) {
throw new InvalidDigestLength(
'Argument 2: Bad HKDF Digest Length'
);
}
// "If [salt] not provided, is set to a string of HashLen zeroes."
if (empty($salt)) {
// @codeCoverageIgnoreStart
$salt = \str_repeat("\x00", \SODIUM_CRYPTO_GENERICHASH_KEYBYTES);
// @codeCoverageIgnoreEnd
}
// HKDF-Extract:
// PRK = HMAC-Hash(salt, IKM)
// The salt is the HMAC key.
$prk = self::raw_keyed_hash($ikm, $salt);
// HKDF-Expand:
// This check is useless, but it serves as a reminder to the spec.
// @codeCoverageIgnoreStart
if (Binary::safeStrlen($prk) < \SODIUM_CRYPTO_GENERICHASH_KEYBYTES) {
throw new CannotPerformOperation(
'An unknown error has occurred'
);
}
// @codeCoverageIgnoreEnd
// T(0) = ''
$t = '';
$last_block = '';
for ($block_index = 1; Binary::safeStrlen($t) < $length; ++$block_index) {
// T(i) = HMAC-Hash(PRK, T(i-1) | info | 0x??)
$last_block = self::raw_keyed_hash(
$last_block . $info . \chr($block_index),
$prk
);
// T = T(1) | T(2) | T(3) | ... | T(N)
$t .= $last_block;
}
// ORM = first L octets of T
/** @var string $orm */
$orm = Binary::safeSubstr($t, 0, $length);
return $orm;
}
/**
* Convert an array of integers to a string
*
* @param array $integers
* @return string
*/
public static function intArrayToString(array $integers): string
{
$args = $integers;
foreach ($args as $i => $v) {
$args[$i] = (int) ($v & 0xff);
}
return \pack(
\str_repeat('C', \count($args)),
...$args
);
}
/**
* Convert an integer to a string (without cache-timing side-channels)
*
* @param int $int
* @return string
*/
public static function intToChr(int $int): string
{
return \pack('C', $int);
}
/**
* Wrapper around SODIUM_CRypto_generichash()
*
* Expects a key (binary string).
* Returns hexadecimal characters.
*
* @param string $input
* @param string $key
* @param int $length
* @return string
* @throws CannotPerformOperation
* @throws \TypeError
* @throws \SodiumException
*/
public static function keyed_hash(
string $input,
string $key,
int $length = \SODIUM_CRYPTO_GENERICHASH_BYTES
): string {
return Hex::encode(
self::raw_keyed_hash($input, $key, $length)
);
}
/**
* Wrapper around SODIUM_CRypto_generichash()
*
* Expects a key (binary string).
* Returns raw binary.
*
* @param string $input
* @param string $key
* @param int $length
* @return string
* @throws CannotPerformOperation
* @throws \SodiumException
*/
public static function raw_keyed_hash(
string $input,
string $key,
int $length = \SODIUM_CRYPTO_GENERICHASH_BYTES
): string {
if ($length < \SODIUM_CRYPTO_GENERICHASH_BYTES_MIN) {
throw new CannotPerformOperation(
\sprintf(
'Output length must be at least %d bytes.',
\SODIUM_CRYPTO_GENERICHASH_BYTES_MIN
)
);
}
if ($length > \SODIUM_CRYPTO_GENERICHASH_BYTES_MAX) {
throw new CannotPerformOperation(
\sprintf(
'Output length must be at most %d bytes.',
\SODIUM_CRYPTO_GENERICHASH_BYTES_MAX
)
);
}
return \sodium_crypto_generichash($input, $key, $length);
}
/**
* PHP 7 uses interned strings. We don't want altering this one to alter
* the original string.
*
* @param string $string
* @return string
* @throws \TypeError
*/
public static function safeStrcpy(string $string): string
{
$length = Binary::safeStrlen($string);
$return = '';
/** @var int $chunk */
$chunk = $length >> 1;
if ($chunk < 1) {
$chunk = 1;
}
for ($i = 0; $i < $length; $i += $chunk) {
$return .= Binary::safeSubstr($string, $i, $chunk);
}
return $return;
}
/**
* Turn a string into an array of integers
*
* @param string $string
* @return array
* @throws \TypeError
*/
public static function stringToIntArray(string $string): array
{
/**
* @var array
*/
$values = \array_values(\unpack('C*', $string));
return $values;
}
/**
* Calculate A xor B, given two binary strings of the same length.
*
* Uses pack() and unpack() to avoid cache-timing leaks caused by
* chr().
*
* @param string $left
* @param string $right
* @return string
* @throws InvalidType
*/
public static function xorStrings(string $left, string $right): string
{
$length = Binary::safeStrlen($left);
if ($length !== Binary::safeStrlen($right)) {
throw new InvalidType(
'Both strings must be the same length'
);
}
if ($length < 1) {
return '';
}
return (string) ($left ^ $right);
}
}
src/Contract/StreamInterface.php 0000666 00000003426 13536431632 0012714 0 ustar 00 getString()) !== \SODIUM_CRYPTO_SIGN_SECRETKEYBYTES) {
throw new InvalidKey(
'Signature secret key must be CRYPTO_SIGN_SECRETKEYBYTES bytes long'
);
}
parent::__construct($keyMaterial);
$this->isSigningKey = true;
}
/**
* See the appropriate derived class.
*
* @return SignaturePublicKey
* @throws InvalidKey
* @throws \TypeError
*/
public function derivePublicKey()
{
$publicKey = \sodium_crypto_sign_publickey_from_secretkey(
$this->getRawKeyMaterial()
);
return new SignaturePublicKey(new HiddenString($publicKey));
}
/**
* Get an encryption secret key from a signing secret key.
*
* @return EncryptionSecretKey
* @throws InvalidKey
* @throws \TypeError
*/
public function getEncryptionSecretKey(): EncryptionSecretKey
{
$ed25519_sk = $this->getRawKeyMaterial();
$x25519_sk = \sodium_crypto_sign_ed25519_sk_to_curve25519(
$ed25519_sk
);
return new EncryptionSecretKey(
new HiddenString($x25519_sk)
);
}
}
src/Asymmetric/SecretKey.php 0000666 00000002054 13536431632 0012072 0 ustar 00 isAsymmetricKey = true;
}
/**
* See the appropriate derived class.
* @throws CannotPerformOperation
* @return mixed
* @codeCoverageIgnore
*/
public function derivePublicKey()
{
throw new CannotPerformOperation(
'This is not implemented in the base class'
);
}
}
src/Asymmetric/PublicKey.php 0000666 00000001404 13536431632 0012061 0 ustar 00 isAsymmetricKey = true;
$this->isPublicKey = true;
}
}
src/Asymmetric/SignaturePublicKey.php 0000666 00000003073 13536431632 0013747 0 ustar 00 getString()) !== \SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES) {
throw new InvalidKey(
'Signature public key must be CRYPTO_SIGN_PUBLICKEYBYTES bytes long'
);
}
parent::__construct($keyMaterial);
$this->isSigningKey = true;
}
/**
* Get an encryption public key from a signing public key.
*
* @return EncryptionPublicKey
* @throws \TypeError
* @throws InvalidKey
*/
public function getEncryptionPublicKey(): EncryptionPublicKey
{
$ed25519_pk = $this->getRawKeyMaterial();
$x25519_pk = \sodium_crypto_sign_ed25519_pk_to_curve25519(
$ed25519_pk
);
return new EncryptionPublicKey(
new HiddenString($x25519_pk)
);
}
}
src/Asymmetric/Crypto.php 0000666 00000036014 13536431632 0011457 0 ustar 00 getRawKeyMaterial(),
$publicKey->getRawKeyMaterial()
)
)
);
}
return new HiddenString(
\sodium_crypto_scalarmult(
$privateKey->getRawKeyMaterial(),
$publicKey->getRawKeyMaterial()
)
);
}
/**
* Encrypt a message with a target users' public key
*
* @param HiddenString $plaintext Message to encrypt
* @param EncryptionPublicKey $publicKey Public encryption key
* @param mixed $encoding Which encoding scheme to use?
* @return string Ciphertext
*
* @throws InvalidType
* @throws \TypeError
*/
public static function seal(
HiddenString $plaintext,
EncryptionPublicKey $publicKey,
$encoding = Halite::ENCODE_BASE64URLSAFE
): string {
$sealed = \sodium_crypto_box_seal(
$plaintext->getString(),
$publicKey->getRawKeyMaterial()
);
$encoder = Halite::chooseEncoder($encoding);
if ($encoder) {
return (string) $encoder($sealed);
}
return (string) $sealed;
}
/**
* Sign a message with our private key
*
* @param string $message Message to sign
* @param SignatureSecretKey $privateKey Private signing key
* @param mixed $encoding Which encoding scheme to use?
* @return string Signature (detached)
*
* @throws InvalidType
* @throws \TypeError
*/
public static function sign(
string $message,
SignatureSecretKey $privateKey,
$encoding = Halite::ENCODE_BASE64URLSAFE
): string {
$signed = \sodium_crypto_sign_detached(
$message,
$privateKey->getRawKeyMaterial()
);
$encoder = Halite::chooseEncoder($encoding);
if ($encoder) {
return (string) $encoder($signed);
}
return (string) $signed;
}
/**
* Sign a message then encrypt it with the recipient's public key.
*
* @param HiddenString $message Plaintext message to sign and encrypt
* @param SignatureSecretKey $secretKey Private signing key
* @param PublicKey $recipientPublicKey Public encryption key
* @param string|bool $encoding Which encoding scheme to use?
* @return string
*
* @throws CannotPerformOperation
* @throws InvalidDigestLength
* @throws InvalidKey
* @throws InvalidMessage
* @throws InvalidType
* @throws \TypeError
*/
public static function signAndEncrypt(
HiddenString $message,
SignatureSecretKey $secretKey,
PublicKey $recipientPublicKey,
$encoding = Halite::ENCODE_BASE64URLSAFE
): string {
if ($recipientPublicKey instanceof SignaturePublicKey) {
$publicKey = $recipientPublicKey->getEncryptionPublicKey();
} elseif ($recipientPublicKey instanceof EncryptionPublicKey) {
$publicKey = $recipientPublicKey;
} else {
// @codeCoverageIgnoreStart
throw new InvalidKey('An invalid key type was provided');
// @codeCoverageIgnoreEnd
}
$signature = self::sign($message->getString(), $secretKey, true);
$plaintext = new HiddenString($signature . $message->getString());
\sodium_memzero($signature);
$myEncKey = $secretKey->getEncryptionSecretKey();
return self::encrypt($plaintext, $myEncKey, $publicKey, $encoding);
}
/**
* Decrypt a sealed message with our private key
*
* @param string $ciphertext Encrypted message
* @param EncryptionSecretKey $privateKey Private decryption key
* @param mixed $encoding Which encoding scheme to use?
* @return HiddenString
*
* @throws InvalidKey
* @throws InvalidMessage
* @throws InvalidType
* @throws \TypeError
*/
public static function unseal(
string $ciphertext,
EncryptionSecretKey $privateKey,
$encoding = Halite::ENCODE_BASE64URLSAFE
): HiddenString {
$decoder = Halite::chooseEncoder($encoding, true);
if ($decoder) {
// We were given hex data:
try {
/** @var string $ciphertext */
$ciphertext = $decoder($ciphertext);
} catch (\RangeException $ex) {
throw new InvalidMessage(
'Invalid character encoding'
);
}
}
// Get a box keypair (needed by crypto_box_seal_open)
$secret_key = $privateKey->getRawKeyMaterial();
$public_key = \sodium_crypto_box_publickey_from_secretkey($secret_key);
$key_pair = \sodium_crypto_box_keypair_from_secretkey_and_publickey(
$secret_key,
$public_key
);
// Wipe these immediately:
\sodium_memzero($secret_key);
\sodium_memzero($public_key);
// Now let's open that sealed box
$message = \sodium_crypto_box_seal_open(
$ciphertext,
$key_pair
);
// Always memzero after retrieving a value
\sodium_memzero($key_pair);
if (!\is_string($message)) {
// @codeCoverageIgnoreStart
throw new InvalidKey(
'Incorrect secret key for this sealed message'
);
// @codeCoverageIgnoreEnd
}
// We have our encrypted message here
return new HiddenString($message);
}
/**
* Verify a signed message with the correct public key
*
* @param string $message Message to verify
* @param SignaturePublicKey $publicKey Public key
* @param string $signature Signature
* @param mixed $encoding Which encoding scheme to use?
* @return bool
*
* @throws InvalidSignature
* @throws InvalidType
* @throws \TypeError
*/
public static function verify(
string $message,
SignaturePublicKey $publicKey,
string $signature,
$encoding = Halite::ENCODE_BASE64URLSAFE
): bool {
$decoder = Halite::chooseEncoder($encoding, true);
if ($decoder) {
// We were given hex data:
/** @var string $signature */
$signature = $decoder($signature);
}
if (Binary::safeStrlen($signature) !== SODIUM_CRYPTO_SIGN_BYTES) {
// @codeCoverageIgnoreStart
throw new InvalidSignature(
'Signature is not the correct length; is it encoded?'
);
// @codeCoverageIgnoreEnd
}
return (bool) \sodium_crypto_sign_verify_detached(
$signature,
$message,
$publicKey->getRawKeyMaterial()
);
}
/**
* Decrypt a message, then verify its signature.
*
* @param string $ciphertext Plaintext message to sign and encrypt
* @param SignaturePublicKey $senderPublicKey Private signing key
* @param SecretKey $givenSecretKey Public encryption key
* @param string|bool $encoding Which encoding scheme to use?
* @return HiddenString
*
* @throws CannotPerformOperation
* @throws InvalidDigestLength
* @throws InvalidKey
* @throws InvalidMessage
* @throws InvalidSignature
* @throws InvalidType
* @throws \TypeError
*/
public static function verifyAndDecrypt(
string $ciphertext,
SignaturePublicKey $senderPublicKey,
SecretKey $givenSecretKey,
$encoding = Halite::ENCODE_BASE64URLSAFE
): HiddenString {
if ($givenSecretKey instanceof SignatureSecretKey) {
$secretKey = $givenSecretKey->getEncryptionSecretKey();
} elseif ($givenSecretKey instanceof EncryptionSecretKey) {
$secretKey = $givenSecretKey;
} else {
throw new InvalidKey('An invalid key type was provided');
}
$senderEncKey = $senderPublicKey->getEncryptionPublicKey();
$decrypted = self::decrypt($ciphertext, $secretKey, $senderEncKey, $encoding);
$signature = Binary::safeSubstr($decrypted->getString(), 0, SODIUM_CRYPTO_SIGN_BYTES);
$message = Binary::safeSubstr($decrypted->getString(), SODIUM_CRYPTO_SIGN_BYTES);
if (!self::verify($message, $senderPublicKey, $signature, true)) {
throw new InvalidSignature('Invalid signature for decrypted message');
}
return new HiddenString($message);
}
}
src/Asymmetric/EncryptionSecretKey.php 0000666 00000002675 13536431632 0014156 0 ustar 00 getString()) !== \SODIUM_CRYPTO_BOX_SECRETKEYBYTES) {
throw new InvalidKey(
'Encryption secret key must be CRYPTO_BOX_SECRETKEYBYTES bytes long'
);
}
parent::__construct($keyMaterial);
}
/**
* See the appropriate derived class.
*
* @return EncryptionPublicKey
*
* @throws InvalidKey
* @throws \TypeError
*/
public function derivePublicKey()
{
$publicKey = \sodium_crypto_box_publickey_from_secretkey(
$this->getRawKeyMaterial()
);
return new EncryptionPublicKey(
new HiddenString($publicKey)
);
}
}
src/Asymmetric/EncryptionPublicKey.php 0000666 00000002051 13536431632 0014133 0 ustar 00 getString()) !== \SODIUM_CRYPTO_BOX_PUBLICKEYBYTES) {
throw new InvalidKey(
'Encryption public key must be CRYPTO_BOX_PUBLICKEYBYTES bytes long'
);
}
parent::__construct($keyMaterial);
}
}
src/Alerts/HaliteAlertInterface.php 0000666 00000000670 13536431632 0013332 0 ustar 00 $keys
*
* @throws InvalidKey
* @throws \InvalidArgumentException
* @throws \TypeError
*/
public function __construct(Key ...$keys)
{
switch (\count($keys)) {
/**
* If we received two keys, it must be an asymmetric secret key and
* an asymmetric public key, in either order.
*/
case 2:
if (!$keys[0]->isAsymmetricKey() || !$keys[1]->isAsymmetricKey()) {
throw new InvalidKey(
'Only keys intended for asymmetric cryptography can be used in a KeyPair object'
);
}
if ($keys[0]->isPublicKey()) {
if ($keys[1]->isPublicKey()) {
throw new InvalidKey(
'Both keys cannot be public keys'
);
}
$this->setupKeyPair(
// @codeCoverageIgnoreStart
$keys[1] instanceof EncryptionSecretKey
? $keys[1]
: new EncryptionSecretKey(
new HiddenString($keys[1]->getRawKeyMaterial())
)
// @codeCoverageIgnoreEnd
);
} elseif ($keys[1]->isPublicKey()) {
$this->setupKeyPair(
// @codeCoverageIgnoreStart
$keys[0] instanceof EncryptionSecretKey
? $keys[0]
: new EncryptionSecretKey(
new HiddenString($keys[0]->getRawKeyMaterial())
)
// @codeCoverageIgnoreEnd
);
} else {
throw new InvalidKey(
'Both keys cannot be secret keys'
);
}
break;
/**
* If we only received one key, it must be an asymmetric secret key!
*/
case 1:
if (!$keys[0]->isAsymmetricKey()) {
throw new InvalidKey(
'Only keys intended for asymmetric cryptography can be used in a KeyPair object'
);
}
if ($keys[0]->isPublicKey()) {
// Ever heard of the Elliptic Curve Discrete Logarithm Problem?
throw new InvalidKey(
'We cannot generate a valid keypair given only a public key; we can given only a secret key, however.'
);
}
$this->setupKeyPair(
// @codeCoverageIgnoreStart
$keys[0] instanceof EncryptionSecretKey
? $keys[0]
: new EncryptionSecretKey(
new HiddenString($keys[0]->getRawKeyMaterial())
)
// @codeCoverageIgnoreEnd
);
break;
default:
throw new \InvalidArgumentException(
'Halite\\EncryptionKeyPair expects 1 or 2 keys'
);
}
}
/**
* Set up our key pair
*
* @param EncryptionSecretKey $secret
* @return void
*
* @throws InvalidKey
* @throws \TypeError
*/
protected function setupKeyPair(EncryptionSecretKey $secret): void
{
$this->secretKey = $secret;
$this->publicKey = $this->secretKey->derivePublicKey();
}
/**
* Get a Key object for the public key
*
* @return EncryptionPublicKey
*/
public function getPublicKey()
{
return $this->publicKey;
}
/**
* Get a Key object for the public key
*
* @return EncryptionSecretKey
*/
public function getSecretKey()
{
return $this->secretKey;
}
}
src/Structure/TrimmedMerkleTree.php 0000666 00000006715 13536431632 0013450 0 ustar 00 nodes);
if ($size < 1) {
return '';
}
/** @var array $hash */
$hash = [];
// Population (Use self::MERKLE_LEAF as a prefix)
for ($i = 0; $i < $size; ++$i) {
$hash[$i] = self::MERKLE_LEAF .
$this->personalization .
$this->nodes[$i]->getHash(
true,
$this->outputSize,
$this->personalization
);
}
// Calculation (Use self::MERKLE_BRANCH as a prefix)
do {
/** @var array $tmp */
$tmp = [];
$j = 0;
for ($i = 0; $i < $size; $i += 2) {
if (empty($hash[$i + 1])) {
$tmp[$j] = $hash[$i];
} elseif(!empty($hash[$i])) {
$tmp[$j] = Util::raw_hash(
self::MERKLE_BRANCH .
$this->personalization .
$hash[$i] .
$hash[$i + 1],
$this->outputSize
);
}
++$j;
}
/** @var array $hash */
$hash = $tmp;
$size >>= 1;
} while ($size > 1);
// We should only have one value left:
$this->rootCalculated = true;
/** @var string $first */
$first = \array_shift($hash);
return $first;
}
/**
* Merkle Trees are immutable. Return a replacement with extra nodes.
*
* @param array $nodes
* @return TrimmedMerkleTree
* @throws InvalidDigestLength
*/
public function getExpandedTree(Node ...$nodes): MerkleTree
{
$thisTree = $this->nodes;
foreach ($nodes as $node) {
$thisTree []= $node;
}
$new = new TrimmedMerkleTree(...$thisTree);
$new->setHashSize($this->outputSize);
$new->setPersonalizationString($this->personalization);
return $new;
}
}
src/Structure/Node.php 0000666 00000004005 13536431632 0010742 0 ustar 00 data = $data;
}
/**
* Get the data
*
* @return string
*/
public function getData(): string
{
return $this->data;
}
/**
* Get a hash of the data (defaults to hex encoded)
*
* @param bool $raw
*
* These two aren't really meant to be used externally:
* @param int $outputSize
* @param string $personalization
*
* @return string
* @throws CannotPerformOperation
* @throws \TypeError
*/
public function getHash(
bool $raw = false,
int $outputSize = \SODIUM_CRYPTO_GENERICHASH_BYTES,
string $personalization = ''
): string {
if ($raw) {
return Util::raw_hash(
$personalization . $this->data,
$outputSize
);
}
return Util::hash(
$personalization . $this->data,
$outputSize
);
}
/**
* Nodes are immutable, but you can create one with extra data.
*
* @param string $concat
* @return Node
*/
public function getExpandedNode(string $concat): Node
{
return new Node($this->data . $concat);
}
}
src/Structure/MerkleTree.php 0000666 00000016335 13536431632 0012125 0 ustar 00 $nodes
*/
public function __construct(Node ...$nodes)
{
$this->nodes = $nodes;
}
/**
* Get the root hash of this Merkle tree.
*
* @param bool $raw - Do we want a raw string instead of a hex string?
*
* @return string
* @throws CannotPerformOperation
*
* @return string
* @throws CannotPerformOperation
* @throws \TypeError
*/
public function getRoot(bool $raw = false): string
{
if (!$this->rootCalculated) {
$this->root = $this->calculateRoot();
}
return $raw
? $this->root
: Hex::encode($this->root);
}
/**
* Merkle Trees are immutable. Return a replacement with extra nodes.
*
* @param array $nodes
* @return MerkleTree
* @throws InvalidDigestLength
*/
public function getExpandedTree(Node ...$nodes): MerkleTree
{
$thisTree = $this->nodes;
foreach ($nodes as $node) {
$thisTree []= $node;
}
return (new MerkleTree(...$thisTree))
->setHashSize($this->outputSize)
->setPersonalizationString($this->personalization);
}
/**
* Set the hash output size.
*
* @param int $size
* @return self
* @throws InvalidDigestLength
*/
public function setHashSize(int $size): self
{
if ($size < \SODIUM_CRYPTO_GENERICHASH_BYTES_MIN) {
throw new InvalidDigestLength(
\sprintf(
'Merkle roots must be at least %d long.',
\SODIUM_CRYPTO_GENERICHASH_BYTES_MIN
)
);
}
if ($size > \SODIUM_CRYPTO_GENERICHASH_BYTES_MAX) {
throw new InvalidDigestLength(
\sprintf(
'Merkle roots must be at most %d long.',
\SODIUM_CRYPTO_GENERICHASH_BYTES_MAX
)
);
}
if ($this->outputSize !== $size) {
$this->rootCalculated = false;
}
$this->outputSize = $size;
return $this;
}
/**
* Sets the personalization string for the Merkle root calculation
*
* @param string $str
* @return self
*/
public function setPersonalizationString(string $str = ''): self
{
if ($this->personalization !== $str) {
$this->rootCalculated = false;
}
$this->personalization = $str;
return $this;
}
/**
* Explicitly recalculate the Merkle root
*
* @return self
* @throws CannotPerformOperation
* @throws \TypeError
* @codeCoverageIgnore
*/
public function triggerRootCalculation(): self
{
$this->root = $this->calculateRoot();
return $this;
}
/**
* Calculate the Merkle root, taking care to distinguish between
* leaves and branches (0x01 for the nodes, 0x00 for the branches)
* to protect against second-preimage attacks
*
* @return string
* @throws CannotPerformOperation
* @throws \TypeError
*/
protected function calculateRoot(): string
{
$size = \count($this->nodes);
if ($size < 1) {
return '';
}
$order = self::getSizeRoundedUp($size);
/** @var array $hash */
$hash = [];
// Population (Use self::MERKLE_LEAF as a prefix)
for ($i = 0; $i < $order; ++$i) {
if ($i >= $size) {
$hash[$i] = self::MERKLE_LEAF .
$this->personalization .
$this->nodes[$size - 1]->getHash(
true,
$this->outputSize,
$this->personalization
);
} else {
$hash[$i] = self::MERKLE_LEAF .
$this->personalization .
$this->nodes[$i]->getHash(
true,
$this->outputSize,
$this->personalization
);
}
}
// Calculation (Use self::MERKLE_BRANCH as a prefix)
do {
/** @var array $tmp */
$tmp = [];
$j = 0;
for ($i = 0; $i < $order; $i += 2) {
/** @var string $prev */
$curr = (string) ($hash[$i] ?? '');
if (empty($hash[$i + 1])) {
// @codeCoverageIgnoreStart
$tmp[$j] = Util::raw_hash(
self::MERKLE_BRANCH .
$this->personalization .
$curr .
$curr,
$this->outputSize
);
// @codeCoverageIgnoreEnd
} else {
/** @var string $curr */
$curr = (string) ($hash[$i] ?? '');
/** @var string $next */
$next = (string) ($hash[$i + 1] ?? '');
$tmp[$j] = Util::raw_hash(
self::MERKLE_BRANCH .
$this->personalization .
$curr . $next,
$this->outputSize
);
}
++$j;
}
$hash = $tmp;
$order >>= 1;
} while ($order > 1);
// We should only have one value left:t
$this->rootCalculated = true;
/** @var string $first */
$first = \array_shift($hash);
return $first;
}
/**
* Let's go ahead and round up to the nearest multiple of 2
*
* @param int $inputSize
* @return int
*/
public static function getSizeRoundedUp(int $inputSize): int
{
$order = 1;
while($order < $inputSize) {
$order <<= 1;
}
return $order;
}
}
src/KeyPair.php 0000666 00000002726 13536431632 0007431 0 ustar 00 '**protected**',
'publicKey' => '**protected**'
];
}
/**
* Get a Key object for the public key
*
* @return PublicKey
* @codeCoverageIgnore
*/
public function getPublicKey()
{
return $this->publicKey;
}
/**
* Get a Key object for the secret key
*
* @return SecretKey
* @codeCoverageIgnore
*/
public function getSecretKey()
{
return $this->secretKey;
}
}
src/Config.php 0000666 00000002713 13536431632 0007266 0 ustar 00 config = $set;
}
/**
* Getter
*
* @param string $key
* @return mixed
* @throws ConfigDirectiveNotFound
*/
public function __get(string $key)
{
if (\array_key_exists($key, $this->config)) {
return $this->config[$key];
}
throw new ConfigDirectiveNotFound($key);
}
/**
* Setter (NOP)
*
* @param string $key
* @param mixed $value
* @return bool
* @codeCoverageIgnore
*/
public function __set(string $key, $value = null)
{
return false;
}
}
src/Halite.php 0000666 00000010645 13536431632 0007272 0 ustar 00 3.141 (approx. pi)
* \x31\x42 => 3.142 (approx. pi)
* Because pi is the symbol we use for Paragon Initiative Enterprises
* \x00\x07 => version 0.07
*
* This library makes heavy use of return-type declarations,
* which are a PHP 7 only feature. Read more about them here:
*
* @ref http://php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration
*
* @package ParagonIE\Halite
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
final class Halite
{
const VERSION = '4.4.0';
const HALITE_VERSION_KEYS = "\x31\x40\x04\x00";
const HALITE_VERSION_FILE = "\x31\x41\x04\x00";
const HALITE_VERSION = "\x31\x42\x04\x00";
/* Raw bytes (decoded) of the underlying ciphertext */
const VERSION_TAG_LEN = 4;
const VERSION_PREFIX = 'MUIEA';
const VERSION_OLD_PREFIX = 'MUIDA';
const ENCODE_HEX = 'hex';
const ENCODE_BASE32 = 'base32';
const ENCODE_BASE32HEX = 'base32hex';
const ENCODE_BASE64 = 'base64';
const ENCODE_BASE64URLSAFE = 'base64urlsafe';
/**
* Don't allow this to be instantiated.
*
* @throws \Error
* @codeCoverageIgnore
*/
final private function __construct()
{
throw new \Error('Do not instantiate');
}
/**
* Select which encoding/decoding function to use.
*
* @internal
* @param mixed $chosen
* @param bool $decode
* @return callable|null
* @throws InvalidType
*/
public static function chooseEncoder($chosen, bool $decode = false)
{
if ($chosen === true) {
return null;
} elseif ($chosen === false) {
return \implode(
'::',
[
Hex::class,
$decode ? 'decode' : 'encode'
]
);
} elseif ($chosen === self::ENCODE_BASE32) {
return \implode(
'::',
[
Base32::class,
$decode ? 'decode' : 'encode'
]
);
} elseif ($chosen === self::ENCODE_BASE32HEX) {
return \implode(
'::',
[
Base32Hex::class,
$decode ? 'decode' : 'encode'
]
);
} elseif ($chosen === self::ENCODE_BASE64) {
return \implode(
'::',
[
Base64::class,
$decode ? 'decode' : 'encode'
]
);
} elseif ($chosen === self::ENCODE_BASE64URLSAFE) {
return \implode(
'::',
[
Base64UrlSafe::class,
$decode ? 'decode' : 'encode'
]
);
} elseif ($chosen === self::ENCODE_HEX) {
return \implode(
'::',
[
Hex::class,
$decode ? 'decode' : 'encode'
]
);
}
throw new InvalidType(
'Illegal value for encoding choice.'
);
}
/**
* Is Libsodium set up correctly? Use this to verify that you can use the
* newer versions of Halite correctly.
*
* @param bool $echo
* @return bool
* @codeCoverageIgnore
*/
public static function isLibsodiumSetupCorrectly(bool $echo = false): bool
{
if (!\extension_loaded('sodium')) {
if ($echo) {
echo "You do not have the sodium extension enabled.\n";
}
return false;
}
// Require libsodium 1.0.15
$major = \SODIUM_LIBRARY_MAJOR_VERSION;
if ($major < 10) {
if ($echo) {
echo 'Halite needs libsodium 1.0.15 or higher. You have: ',
\SODIUM_LIBRARY_VERSION, "\n";
}
return false;
}
return true;
}
}
src/Stream/MutableFile.php 0000666 00000017353 13536431632 0011513 0 ustar 00 fp = $fp;
$this->closeAfter = true;
$this->pos = 0;
$this->stat = \fstat($this->fp);
} elseif (\is_resource($file)) {
/** @var array $metadata */
$metadata = \stream_get_meta_data($file);
if (!\in_array($metadata['mode'], self::ALLOWED_MODES, true)) {
throw new FileAccessDenied(
'Resource is in ' . $metadata['mode'] . ' mode, which is not allowed.'
);
}
$this->fp = $file;
$this->pos = \ftell($this->fp);
$this->stat = \fstat($this->fp);
} else {
throw new InvalidType(
'Argument 1: Expected a filename or resource'
);
}
}
/**
* Close the file handle.
*/
public function close(): void
{
if ($this->closeAfter) {
$this->closeAfter = false;
\fclose($this->fp);
\clearstatcache();
}
}
/**
* Make sure we invoke $this->close()
*/
public function __destruct()
{
$this->close();
}
/**
* Where are we in the buffer?
*
* @return int
*/
public function getPos(): int
{
return \ftell($this->fp);
}
/**
* How big is this buffer?
*
* @return int
*/
public function getSize(): int
{
$stat = \fstat($this->fp);
return (int) $stat['size'];
}
/**
* Get information about the stream.
*
* @return array
*/
public function getStreamMetadata(): array
{
return \stream_get_meta_data($this->fp);
}
/**
* Read from a stream; prevent partial reads
*
* @param int $num
* @param bool $skipTests
* @return string
* @throws CannotPerformOperation
* @throws FileAccessDenied
*/
public function readBytes(int $num, bool $skipTests = false): string
{
// @codeCoverageIgnoreStart
if ($num < 0) {
throw new CannotPerformOperation('num < 0');
} elseif ($num === 0) {
return '';
}
if (($this->pos + $num) > $this->stat['size']) {
throw new CannotPerformOperation('Out-of-bounds read');
}
// @codeCoverageIgnoreEnd
$buf = '';
$remaining = $num;
do {
if ($remaining <= 0) {
// @codeCoverageIgnoreStart
break;
// @codeCoverageIgnoreEnd
}
/** @var int $bufSize */
$bufSize = \min($remaining, self::CHUNK);
/** @var string|bool $read */
$read = \fread($this->fp, $bufSize);
if (!\is_string($read)) {
// @codeCoverageIgnoreStart
throw new FileAccessDenied(
'Could not read from the file'
);
// @codeCoverageIgnoreEnd
}
$buf .= $read;
$readSize = Binary::safeStrlen($read);
$this->pos += $readSize;
$remaining -= $readSize;
} while ($remaining > 0);
return $buf;
}
/**
* Get number of bytes remaining
*
* @return int
*/
public function remainingBytes(): int
{
/** @var array $stat */
$stat = \fstat($this->fp);
/** @var int $pos */
$pos = \ftell($this->fp);
return (int) (
PHP_INT_MAX & (
(int) $stat['size'] - $pos
)
);
}
/**
* Set the current cursor position to the desired location
*
* @param int $i
* @return bool
* @throws CannotPerformOperation
* @codeCoverageIgnore
*/
public function reset(int $i = 0): bool
{
$this->pos = $i;
if (\fseek($this->fp, $i, SEEK_SET) === 0) {
return true;
}
throw new CannotPerformOperation(
'fseek() failed'
);
}
/**
* Write to a stream; prevent partial writes
*
* @param string $buf
* @param int|null $num (number of bytes)
* @return int
*
* @throws CannotPerformOperation
* @throws FileAccessDenied
* @throws \TypeError
*/
public function writeBytes(string $buf, int $num = null): int
{
$bufSize = Binary::safeStrlen($buf);
if (!\is_int($num) || $num > $bufSize) {
$num = $bufSize;
}
// @codeCoverageIgnoreStart
if ($num < 0) {
throw new CannotPerformOperation('num < 0');
}
// @codeCoverageIgnoreEnd
$remaining = $num;
do {
// @codeCoverageIgnoreStart
if ($remaining <= 0) {
break;
}
// @codeCoverageIgnoreEnd
$written = \fwrite($this->fp, $buf, $remaining);
if ($written === false) {
// @codeCoverageIgnoreStart
throw new FileAccessDenied(
'Could not write to the file'
);
// @codeCoverageIgnoreEnd
}
$buf = Binary::safeSubstr($buf, $written, null);
$this->pos += $written;
$this->stat = \fstat($this->fp);
$remaining -= $written;
} while ($remaining > 0);
return $num;
}
}
src/Stream/WeakReadOnlyFile.php 0000666 00000000463 13536431632 0012441 0 ustar 00 fp = $fp;
$this->closeAfter = true;
$this->pos = 0;
$this->stat = $this->fstat();
} elseif (\is_resource($file)) {
/** @var array $metadata */
$metadata = \stream_get_meta_data($file);
if (!\in_array($metadata['mode'], (array) static::ALLOWED_MODES, true)) {
throw new FileAccessDenied(
'Resource is in ' . $metadata['mode'] . ' mode, which is not allowed.'
);
}
$this->fp = $file;
$this->pos = \ftell($this->fp);
$this->stat = $this->fstat();
} else {
throw new InvalidType(
'Argument 1: Expected a filename or resource'
);
}
// @codeCoverageIgnoreStart
$this->hashKey = !empty($key)
? $key->getRawKeyMaterial()
: '';
// @codeCoverageIgnoreEnd
$this->hash = $this->getHash();
}
/**
* Make sure we invoke $this->close()
*/
public function __destruct()
{
$this->close();
}
/**
* Close the file handle.
* @return void
*/
public function close(): void
{
if ($this->closeAfter) {
$this->closeAfter = false;
\fclose($this->fp);
\clearstatcache();
}
}
/**
* Calculate a BLAKE2b hash of a file
*
* @return string
*/
public function getHash(): string
{
if ($this->hash) {
$this->toctouTest();
return $this->hash;
}
$init = $this->pos;
\fseek($this->fp, 0, SEEK_SET);
// Create a hash context:
$h = \sodium_crypto_generichash_init(
$this->hashKey,
\SODIUM_CRYPTO_GENERICHASH_BYTES_MAX
);
for ($i = 0; $i < $this->stat['size']; $i += self::CHUNK) {
if (($i + self::CHUNK) > $this->stat['size']) {
$c = \fread($this->fp, ((int) $this->stat['size'] - $i));
} else {
$c = \fread($this->fp, self::CHUNK);
}
if (!\is_string($c)) {
// @codeCoverageIgnoreStart
throw new FileError('Could not read file');
// @codeCoverageIgnoreEnd
}
\sodium_crypto_generichash_update($h, $c);
}
// Reset the file pointer's internal cursor to where it was:
\fseek($this->fp, $init, SEEK_SET);
return \sodium_crypto_generichash_final($h);
}
/**
* Where are we in the buffer?
*
* @return int
*/
public function getPos(): int
{
return $this->pos;
}
/**
* How big is this buffer?
*
* @return int
*/
public function getSize(): int
{
return (int) $this->stat['size'];
}
/**
* Get information about the stream.
*
* @return array
*/
public function getStreamMetadata(): array
{
return \stream_get_meta_data($this->fp);
}
/**
* Read from a stream; prevent partial reads (also uses run-time testing to
* prevent partial reads -- you can turn this off if you need performance
* and aren't concerned about race condition attacks, but this isn't a
* decision to make lightly!)
*
* @param int $num
* @param bool $skipTests Only set this to TRUE if you're absolutely sure
* that you don't want to defend against TOCTOU /
* race condition attacks on the filesystem!
* @return string
* @throws CannotPerformOperation
* @throws FileAccessDenied
* @throws FileModified
*/
public function readBytes(int $num, bool $skipTests = false): string
{
// @codeCoverageIgnoreStart
if ($num < 0) {
throw new CannotPerformOperation('num < 0');
} elseif ($num === 0) {
return '';
}
if (($this->pos + $num) > $this->stat['size']) {
throw new CannotPerformOperation('Out-of-bounds read');
}
$buf = '';
// @codeCoverageIgnoreEnd
$remaining = $num;
if (!$skipTests) {
$this->toctouTest();
}
do {
// @codeCoverageIgnoreStart
if ($remaining <= 0) {
break;
}
// @codeCoverageIgnoreEnd
/** @var string|bool $read */
$read = \fread($this->fp, $remaining);
if (!\is_string($read)) {
// @codeCoverageIgnoreStart
throw new FileAccessDenied(
'Could not read from the file'
);
// @codeCoverageIgnoreEnd
}
$buf .= $read;
$readSize = Binary::safeStrlen($read);
$this->pos += $readSize;
$remaining -= $readSize;
} while ($remaining > 0);
return $buf;
}
/**
* Get number of bytes remaining
*
* @return int
*/
public function remainingBytes(): int
{
return (int) (
PHP_INT_MAX & (
(int) $this->stat['size'] - $this->pos
)
);
}
/**
* Set the current cursor position to the desired location
*
* @param int $position
* @return bool
* @throws CannotPerformOperation
*/
public function reset(int $position = 0): bool
{
$this->pos = $position;
if (\fseek($this->fp, $position, SEEK_SET) === 0) {
return true;
}
// @codeCoverageIgnoreStart
throw new CannotPerformOperation(
'fseek() failed'
);
// @codeCoverageIgnoreEnd
}
/**
* Run-time test to prevent TOCTOU attacks (race conditions) through
* verifying that the hash matches and the current cursor position/file
* size matches their values when the file was first opened.
*
* @throws FileModified
* @return void
*/
public function toctouTest()
{
if (\ftell($this->fp) !== $this->pos) {
// @codeCoverageIgnoreStart
throw new FileModified(
'Read-only file has been modified since it was opened for reading'
);
// @codeCoverageIgnoreEnd
}
$stat = $this->fstat();
if ($stat['size'] !== $this->stat['size']) {
throw new FileModified(
'Read-only file has been modified since it was opened for reading'
);
}
}
/**
* This is a meaningless operation for a Read-Only File!
*
* @param string $buf
* @param int $num (number of bytes)
* @return int
* @throws FileAccessDenied
*/
public function writeBytes(string $buf, int $num = null): int
{
unset($buf);
unset($num);
throw new FileAccessDenied(
'This is a read-only file handle.'
);
}
/**
* Wraps fstat to allow calculation of file-size on stream wrappers.
*
* @return array
*/
private function fstat() : array {
$stat = \fstat($this->fp);
if ($stat) {
return $stat;
}
// The resource is remote or a stream wrapper like php://input
$stat = [
'size' => 0,
];
\fseek($this->fp, 0);
while (!feof($this->fp)) {
$stat['size'] += \strlen(\fread($this->fp, 8192));
}
\fseek($this->fp, $this->pos);
return $stat;
}
}
src/SignatureKeyPair.php 0000666 00000012460 13536431632 0011307 0 ustar 00 $keys
*
* @throws InvalidKey
* @throws \TypeError
*/
public function __construct(Key ...$keys)
{
switch (\count($keys)) {
/**
* If we received two keys, it must be an asymmetric secret key and
* an asymmetric public key, in either order.
*/
case 2:
if (!$keys[0]->isAsymmetricKey() || !$keys[1]->isAsymmetricKey()) {
throw new InvalidKey(
'Only keys intended for asymmetric cryptography can be used in a KeyPair object'
);
}
if ($keys[0]->isPublicKey()) {
if ($keys[1]->isPublicKey()) {
throw new InvalidKey(
'Both keys cannot be public keys'
);
}
$this->setupKeyPair(
// @codeCoverageIgnoreStart
$keys[1] instanceof SignatureSecretKey
? $keys[1]
: new SignatureSecretKey(
new HiddenString($keys[1]->getRawKeyMaterial())
)
// @codeCoverageIgnoreEnd
);
} elseif ($keys[1]->isPublicKey()) {
$this->setupKeyPair(
// @codeCoverageIgnoreStart
$keys[0] instanceof SignatureSecretKey
? $keys[0]
: new SignatureSecretKey(
new HiddenString($keys[0]->getRawKeyMaterial())
)
// @codeCoverageIgnoreEnd
);
} else {
throw new InvalidKey(
'Both keys cannot be secret keys'
);
}
break;
/**
* If we only received one key, it must be an asymmetric secret key!
*/
case 1:
if (!$keys[0]->isAsymmetricKey()) {
throw new InvalidKey(
'Only keys intended for asymmetric cryptography can be used in a KeyPair object'
);
}
if ($keys[0]->isPublicKey()) {
// Ever heard of the Elliptic Curve Discrete Logarithm Problem?
throw new InvalidKey(
'We cannot generate a valid keypair given only a public key; we can given only a secret key, however.'
);
}
$this->setupKeyPair(
// @codeCoverageIgnoreStart
$keys[0] instanceof SignatureSecretKey
? $keys[0]
: new SignatureSecretKey(
new HiddenString($keys[0]->getRawKeyMaterial())
)
// @codeCoverageIgnoreEnd
);
break;
default:
throw new \InvalidArgumentException(
'Halite\\EncryptionKeyPair expects 1 or 2 keys'
);
}
}
/**
* @return EncryptionKeyPair
* @throws InvalidKey
* @throws \TypeError
*/
public function getEncryptionKeyPair(): EncryptionKeyPair
{
return new EncryptionKeyPair(
$this->secretKey->getEncryptionSecretKey(),
$this->publicKey->getEncryptionPublicKey()
);
}
/**
* Set up our key pair
*
* @param SignatureSecretKey $secret
* @return void
*
* @throws InvalidKey
* @throws \TypeError
*/
protected function setupKeyPair(SignatureSecretKey $secret): void
{
$this->secretKey = $secret;
$this->publicKey = $this->secretKey->derivePublicKey();
}
/**
* Get a Key object for the public key
*
* @return SignaturePublicKey
*/
public function getPublicKey()
{
return $this->publicKey;
}
/**
* Get a Key object for the public key
*
* @return SignatureSecretKey
*/
public function getSecretKey()
{
return $this->secretKey;
}
}
src/Symmetric/SecretKey.php 0000666 00000000620 13536431632 0011726 0 ustar 00 Halite::ENCODE_BASE64URLSAFE,
'SHORTEST_CIPHERTEXT_LENGTH' => 124,
'NONCE_BYTES' => \SODIUM_CRYPTO_STREAM_NONCEBYTES,
'HKDF_SALT_LEN' => 32,
'MAC_ALGO' => 'BLAKE2b',
'MAC_SIZE' => \SODIUM_CRYPTO_GENERICHASH_BYTES_MAX,
'HKDF_SBOX' => 'Halite|EncryptionKey',
'HKDF_AUTH' => 'AuthenticationKeyFor_|Halite'
];
}
}
throw new InvalidMessage(
'Invalid version tag'
);
}
/**
* Get the configuration for seal operations
*
* @param int $major
* @param int $minor
* @return array
* @throws InvalidMessage
*/
public static function getConfigAuth(int $major, int $minor): array
{
if ($major === 3 || $major === 4) {
switch ($minor) {
case 0:
return [
'HKDF_SALT_LEN' => 32,
'MAC_ALGO' => 'BLAKE2b',
'MAC_SIZE' => \SODIUM_CRYPTO_GENERICHASH_BYTES_MAX,
'PUBLICKEY_BYTES' => \SODIUM_CRYPTO_BOX_PUBLICKEYBYTES,
'HKDF_SBOX' => 'Halite|EncryptionKey',
'HKDF_AUTH' => 'AuthenticationKeyFor_|Halite'
];
}
}
throw new InvalidMessage(
'Invalid version tag'
);
}
}
src/Symmetric/Crypto.php 0000666 00000037422 13536431632 0011322 0 ustar 00 getRawKeyMaterial(),
$config
);
$encoder = Halite::chooseEncoder($encoding);
if ($encoder) {
return (string) $encoder($mac);
}
return (string) $mac;
}
/**
* Decrypt a message using the Halite encryption protocol
*
* @param string $ciphertext
* @param EncryptionKey $secretKey
* @param mixed $encoding
* @return HiddenString
*
* @throws CannotPerformOperation
* @throws InvalidDigestLength
* @throws InvalidMessage
* @throws InvalidSignature
* @throws InvalidType
* @throws \TypeError
*/
public static function decrypt(
string $ciphertext,
EncryptionKey $secretKey,
$encoding = Halite::ENCODE_BASE64URLSAFE
): HiddenString {
return static::decryptWithAd(
$ciphertext,
$secretKey,
'',
$encoding
);
}
/**
* Decrypt a message using the Halite encryption protocol
*
* @param string $ciphertext
* @param EncryptionKey $secretKey
* @param string $additionalData
* @param mixed $encoding
* @return HiddenString
*
* @throws CannotPerformOperation
* @throws InvalidDigestLength
* @throws InvalidMessage
* @throws InvalidSignature
* @throws InvalidType
* @throws \TypeError
*/
public static function decryptWithAd(
string $ciphertext,
EncryptionKey $secretKey,
string $additionalData = '',
$encoding = Halite::ENCODE_BASE64URLSAFE
): HiddenString {
$decoder = Halite::chooseEncoder($encoding, true);
if (\is_callable($decoder)) {
// We were given encoded data:
// @codeCoverageIgnoreStart
try {
/** @var string $ciphertext */
$ciphertext = $decoder($ciphertext);
} catch (\RangeException $ex) {
throw new InvalidMessage(
'Invalid character encoding'
);
}
// @codeCoverageIgnoreEnd
}
/** @var array $pieces */
$pieces = self::unpackMessageForDecryption($ciphertext);
/** @var string $version */
$version = $pieces[0];
/** @var Config $config */
$config = $pieces[1];
/** @var string $salt */
$salt = $pieces[2];
/** @var string $nonce */
$nonce = $pieces[3];
/** @var string $encrypted */
$encrypted = $pieces[4];
/** @var string $auth */
$auth = $pieces[5];
/* Split our key into two keys: One for encryption, the other for
authentication. By using separate keys, we can reasonably dismiss
likely cross-protocol attacks.
This uses salted HKDF to split the keys, which is why we need the
salt in the first place. */
/**
* @var array $split
* @var string $encKey
* @var string $authKey
*/
$split = self::splitKeys($secretKey, (string) $salt, $config);
$encKey = $split[0];
$authKey = $split[1];
// Check the MAC first
if (!self::verifyMAC(
// @codeCoverageIgnoreStart
(string) $auth,
(string) $version .
(string) $salt .
(string) $nonce .
(string) $additionalData .
(string) $encrypted,
// @codeCoverageIgnoreEnd
$authKey,
$config
)) {
throw new InvalidMessage(
'Invalid message authentication code'
);
}
\sodium_memzero($salt);
\sodium_memzero($authKey);
// crypto_stream_xor() can be used to encrypt and decrypt
/** @var string $plaintext */
$plaintext = \sodium_crypto_stream_xor(
(string) $encrypted,
(string) $nonce,
(string) $encKey
);
\sodium_memzero($encrypted);
\sodium_memzero($nonce);
\sodium_memzero($encKey);
return new HiddenString($plaintext);
}
/**
* Encrypt a message using the Halite encryption protocol
*
* (Encrypt then MAC -- xsalsa20 then keyed-Blake2b)
* You don't need to worry about chosen-ciphertext attacks.
*
* @param HiddenString $plaintext
* @param EncryptionKey $secretKey
* @param string|bool $encoding
* @return string
*
* @throws CannotPerformOperation
* @throws InvalidDigestLength
* @throws InvalidMessage
* @throws InvalidType
* @throws \TypeError
*/
public static function encrypt(
HiddenString $plaintext,
EncryptionKey $secretKey,
$encoding = Halite::ENCODE_BASE64URLSAFE
): string {
return static::encryptWithAd(
$plaintext,
$secretKey,
'',
$encoding
);
}
/**
* @param HiddenString $plaintext
* @param EncryptionKey $secretKey
* @param string $additionalData
* @param string|bool $encoding
* @return string
*
* @throws CannotPerformOperation
* @throws InvalidDigestLength
* @throws InvalidMessage
* @throws InvalidType
* @throws \TypeError
*/
public static function encryptWithAd(
HiddenString $plaintext,
EncryptionKey $secretKey,
string $additionalData = '',
$encoding = Halite::ENCODE_BASE64URLSAFE
): string {
$config = SymmetricConfig::getConfig(Halite::HALITE_VERSION, 'encrypt');
// Generate a nonce and HKDF salt:
// @codeCoverageIgnoreStart
try {
$nonce = \random_bytes(\SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
$salt = \random_bytes((int) $config->HKDF_SALT_LEN);
} catch (\Throwable $ex) {
throw new CannotPerformOperation($ex->getMessage());
}
// @codeCoverageIgnoreEnd
/* Split our key into two keys: One for encryption, the other for
authentication. By using separate keys, we can reasonably dismiss
likely cross-protocol attacks.
This uses salted HKDF to split the keys, which is why we need the
salt in the first place. */
list($encKey, $authKey) = self::splitKeys($secretKey, $salt, $config);
// Encrypt our message with the encryption key:
/** @var string $encrypted */
$encrypted = \sodium_crypto_stream_xor(
$plaintext->getString(),
$nonce,
$encKey
);
\sodium_memzero($encKey);
// Calculate an authentication tag:
$auth = self::calculateMAC(
Halite::HALITE_VERSION . $salt . $nonce . $additionalData . $encrypted,
$authKey,
$config
);
\sodium_memzero($authKey);
/** @var string $message */
$message = Halite::HALITE_VERSION . $salt . $nonce . $encrypted . $auth;
// Wipe every superfluous piece of data from memory
\sodium_memzero($nonce);
\sodium_memzero($salt);
\sodium_memzero($encrypted);
\sodium_memzero($auth);
$encoder = Halite::chooseEncoder($encoding);
if ($encoder) {
return (string) $encoder($message);
}
return (string) $message;
}
/**
* Split a key (using HKDF-BLAKE2b instead of HKDF-HMAC-*)
*
* @param EncryptionKey $master
* @param string $salt
* @param BaseConfig $config
* @return string[]
*
* @throws CannotPerformOperation
* @throws InvalidDigestLength
* @throws \TypeError
*/
public static function splitKeys(
EncryptionKey $master,
string $salt,
BaseConfig $config
): array {
$binary = $master->getRawKeyMaterial();
return [
CryptoUtil::hkdfBlake2b(
$binary,
\SODIUM_CRYPTO_SECRETBOX_KEYBYTES,
(string) $config->HKDF_SBOX,
$salt
),
CryptoUtil::hkdfBlake2b(
$binary,
\SODIUM_CRYPTO_AUTH_KEYBYTES,
(string) $config->HKDF_AUTH,
$salt
)
];
}
/**
* Unpack a message string into an array (assigned to variables via list()).
*
* Should return exactly 6 elements.
*
* @param string $ciphertext
* @return array
*
* @throws InvalidMessage
* @throws \TypeError
* @codeCoverageIgnore
*/
public static function unpackMessageForDecryption(string $ciphertext): array
{
$length = Binary::safeStrlen($ciphertext);
// Fail fast on invalid messages
if ($length < Halite::VERSION_TAG_LEN) {
throw new InvalidMessage(
'Message is too short'
);
}
// The first 4 bytes are reserved for the version size
$version = Binary::safeSubstr(
$ciphertext,
0,
Halite::VERSION_TAG_LEN
);
$config = SymmetricConfig::getConfig($version, 'encrypt');
if ($length < $config->SHORTEST_CIPHERTEXT_LENGTH) {
throw new InvalidMessage(
'Message is too short'
);
}
// The salt is used for key splitting (via HKDF)
$salt = Binary::safeSubstr(
$ciphertext,
Halite::VERSION_TAG_LEN,
(int) $config->HKDF_SALT_LEN
);
// This is the nonce (we authenticated it):
$nonce = Binary::safeSubstr(
$ciphertext,
// 36:
Halite::VERSION_TAG_LEN + (int) $config->HKDF_SALT_LEN,
// 24:
\SODIUM_CRYPTO_STREAM_NONCEBYTES
);
// This is the crypto_stream_xor()ed ciphertext
$encrypted = Binary::safeSubstr(
$ciphertext,
// 60:
Halite::VERSION_TAG_LEN +
(int) $config->HKDF_SALT_LEN +
\SODIUM_CRYPTO_STREAM_NONCEBYTES,
// $length - 124
$length - (
Halite::VERSION_TAG_LEN +
(int) $config->HKDF_SALT_LEN +
\SODIUM_CRYPTO_STREAM_NONCEBYTES +
(int) $config->MAC_SIZE
)
);
// $auth is the last 32 bytes
$auth = Binary::safeSubstr(
$ciphertext,
$length - (int) $config->MAC_SIZE
);
// We don't need this anymore.
\sodium_memzero($ciphertext);
// Now we return the pieces in a specific order:
return [$version, $config, $salt, $nonce, $encrypted, $auth];
}
/**
* Verify the authenticity of a message, given a shared MAC key
*
* @param string $message
* @param AuthenticationKey $secretKey
* @param string $mac
* @param mixed $encoding
* @param SymmetricConfig $config
* @return bool
*
* @throws InvalidMessage
* @throws InvalidSignature
* @throws InvalidType
* @throws \TypeError
*/
public static function verify(
string $message,
AuthenticationKey $secretKey,
string $mac,
$encoding = Halite::ENCODE_BASE64URLSAFE,
SymmetricConfig $config = null
): bool {
$decoder = Halite::chooseEncoder($encoding, true);
if ($decoder) {
// We were given hex data:
/** @var string $mac */
$mac = $decoder($mac);
}
if ($config === null) {
// Default to the current version
$config = SymmetricConfig::getConfig(
Halite::HALITE_VERSION,
'auth'
);
}
try {
return self::verifyMAC(
$mac,
$message,
$secretKey->getRawKeyMaterial(),
$config
);
// @codeCoverageIgnoreStart
} catch (InvalidMessage $ex) {
return false;
// @codeCoverageIgnoreEnd
}
}
/**
* Calculate a MAC. This is used internally.
*
* @param string $message
* @param string $authKey
* @param SymmetricConfig $config
* @return string
* @throws InvalidMessage
*/
protected static function calculateMAC(
string $message,
string $authKey,
SymmetricConfig $config
): string {
if ($config->MAC_ALGO === 'BLAKE2b') {
return \sodium_crypto_generichash(
$message,
$authKey,
(int) $config->MAC_SIZE
);
}
// @codeCoverageIgnoreStart
throw new InvalidMessage(
'Invalid Halite version'
);
// @codeCoverageIgnoreEnd
}
/**
* Verify a Message Authentication Code (MAC) of a message, with a shared
* key.
*
* @param string $mac Message Authentication Code
* @param string $message The message to verify
* @param string $authKey Authentication key (symmetric)
* @param SymmetricConfig $config Configuration object
* @return bool
*
* @throws InvalidMessage
* @throws InvalidSignature
*/
protected static function verifyMAC(
string $mac,
string $message,
string $authKey,
SymmetricConfig $config
): bool {
if (Binary::safeStrlen($mac) !== $config->MAC_SIZE) {
// @codeCoverageIgnoreStart
throw new InvalidSignature(
'Argument 1: Message Authentication Code is not the correct length; is it encoded?'
);
// @codeCoverageIgnoreEnd
}
if ($config->MAC_ALGO === 'BLAKE2b') {
$calc = \sodium_crypto_generichash(
$message,
$authKey,
(int) $config->MAC_SIZE
);
$res = \hash_equals($mac, $calc);
\sodium_memzero($calc);
return $res;
}
// @codeCoverageIgnoreStart
throw new InvalidMessage(
'Invalid Halite version'
);
// @codeCoverageIgnoreEnd
}
}
src/Symmetric/AuthenticationKey.php 0000666 00000002061 13536431632 0013461 0 ustar 00 getString()) !== \SODIUM_CRYPTO_AUTH_KEYBYTES) {
throw new InvalidKey(
'Authentication key must be CRYPTO_AUTH_KEYBYTES bytes long'
);
}
parent::__construct($keyMaterial);
$this->isSigningKey = true;
}
}
src/Symmetric/EncryptionKey.php 0000666 00000001772 13536431632 0012644 0 ustar 00 getString()) !== \SODIUM_CRYPTO_STREAM_KEYBYTES) {
throw new InvalidKey(
'Encryption key must be CRYPTO_STREAM_KEYBYTES bytes long'
);
}
parent::__construct($keyMaterial);
}
}
src/File.php 0000666 00000130017 13536431632 0006737 0 ustar 00 getPos();
$filePath->reset(0);
$checksum = self::checksumData(
$filePath,
$key,
$encoding
);
$filePath->reset($pos);
return $checksum;
}
if (\is_resource($filePath) || \is_string($filePath)) {
$readOnly = new ReadOnlyFile($filePath);
try {
$checksum = self::checksumData(
$readOnly,
$key,
$encoding
);
return $checksum;
} finally {
if (isset($readOnly)) {
$readOnly->close();
}
}
}
throw new InvalidType(
'Argument 1: Expected a filename or resource'
);
}
/**
* Encrypt a file using symmetric authenticated encryption.
*
* @param string|resource|ReadOnlyFile $input Input file
* @param string|resource|MutableFile $output Output file
* @param EncryptionKey $key Symmetric encryption key
* @return int Number of bytes written
*
* @throws CannotPerformOperation
* @throws FileAccessDenied
* @throws FileError
* @throws FileModified
* @throws InvalidDigestLength
* @throws InvalidKey
* @throws InvalidMessage
* @throws InvalidType
* @throws \TypeError
*/
public static function encrypt(
$input,
$output,
EncryptionKey $key
): int {
if (
(\is_resource($input) || \is_string($input) || ($input instanceof ReadOnlyFile))
&&
(\is_resource($output) || \is_string($output) || ($output instanceof MutableFile))
) {
try {
if ($input instanceof ReadOnlyFile) {
$readOnly = $input;
} else {
$readOnly = new ReadOnlyFile($input);
}
if ($output instanceof MutableFile) {
$mutable = $output;
} else {
$mutable = new MutableFile($output);
}
$data = self::encryptData(
$readOnly,
$mutable,
$key
);
return $data;
} finally {
if (isset($readOnly)) {
$readOnly->close();
}
if (isset($mutable)) {
$mutable->close();
}
}
}
throw new InvalidType(
'Argument 1: Expected a filename or resource'
);
}
/**
* Decrypt a file using symmetric-key authenticated encryption.
*
* @param string|resource|ReadOnlyFile $input Input file
* @param string|resource|MutableFile $output Output file
* @param EncryptionKey $key Symmetric encryption key
* @return bool TRUE if successful
*
* @throws CannotPerformOperation
* @throws FileAccessDenied
* @throws FileError
* @throws FileModified
* @throws InvalidDigestLength
* @throws InvalidKey
* @throws InvalidMessage
* @throws InvalidType
* @throws \TypeError
*/
public static function decrypt(
$input,
$output,
EncryptionKey $key
): bool {
if (
(\is_resource($input) || \is_string($input) || ($input instanceof ReadOnlyFile))
&&
(\is_resource($output) || \is_string($output) || ($output instanceof MutableFile))
) {
try {
if ($input instanceof ReadOnlyFile) {
$readOnly = $input;
} else {
$readOnly = new ReadOnlyFile($input);
}
if ($output instanceof MutableFile) {
$mutable = $output;
} else {
$mutable = new MutableFile($output);
}
$data = self::decryptData(
$readOnly,
$mutable,
$key
);
return $data;
} finally {
if (isset($readOnly)) {
$readOnly->close();
}
if (isset($mutable)) {
$mutable->close();
}
}
}
throw new InvalidType(
'Strings or file handles expected'
);
}
/**
* Encrypt a file using anonymous public-key encryption (with ciphertext
* authentication).
*
* @param string|resource|ReadOnlyFile $input Input file
* @param string|resource|MutableFile $output Output file
* @param EncryptionPublicKey $publicKey Recipient's encryption public key
* @return int
*
* @throws CannotPerformOperation
* @throws FileAccessDenied
* @throws FileModified
* @throws InvalidDigestLength
* @throws InvalidMessage
* @throws InvalidType
* @throws \Exception
* @throws \TypeError
*/
public static function seal(
$input,
$output,
EncryptionPublicKey $publicKey
): int {
if (
(\is_resource($input) || \is_string($input) || ($input instanceof ReadOnlyFile))
&&
(\is_resource($output) || \is_string($output) || ($output instanceof MutableFile))
) {
try {
if ($input instanceof ReadOnlyFile) {
$readOnly = $input;
} else {
$readOnly = new ReadOnlyFile($input);
}
if ($output instanceof MutableFile) {
$mutable = $output;
} else {
$mutable = new MutableFile($output);
}
$data = self::sealData(
$readOnly,
$mutable,
$publicKey
);
return $data;
} finally {
if (isset($readOnly)) {
$readOnly->close();
}
if (isset($mutable)) {
$mutable->close();
}
}
}
throw new InvalidType(
'Argument 1: Expected a filename or resource'
);
}
/**
* Decrypt a file using anonymous public-key encryption. Ciphertext
* integrity is still assured thanks to the Encrypt-then-MAC construction.
*
* @param string|resource|ReadOnlyFile $input Input file
* @param string|resource|MutableFile $output Output file
* @param EncryptionSecretKey $secretKey Recipient's encryption secret key
* @return bool TRUE on success
*
* @throws CannotPerformOperation
* @throws FileAccessDenied
* @throws FileError
* @throws FileModified
* @throws InvalidDigestLength
* @throws InvalidKey
* @throws InvalidMessage
* @throws InvalidType
* @throws \TypeError
*/
public static function unseal(
$input,
$output,
EncryptionSecretKey $secretKey
): bool {
if (
(\is_resource($input) || \is_string($input) || ($input instanceof ReadOnlyFile))
&&
(\is_resource($output) || \is_string($output) || ($output instanceof MutableFile))
) {
try {
if ($input instanceof ReadOnlyFile) {
$readOnly = $input;
} else {
$readOnly = new ReadOnlyFile($input);
}
if ($output instanceof MutableFile) {
$mutable = $output;
} else {
$mutable = new MutableFile($output);
}
$data = self::unsealData(
$readOnly,
$mutable,
$secretKey
);
return $data;
} finally {
if (isset($readOnly)) {
$readOnly->close();
}
if (isset($mutable)) {
$mutable->close();
}
}
}
throw new InvalidType(
'Argument 1: Expected a filename or resource'
);
}
/**
* Calculate a digital signature (Ed25519) of a file
*
* Specifically:
* 1. Calculate the BLAKE2b-512 checksum of the file, with the signer's
* Ed25519 public key used as a BLAKE2b key.
* 2. Sign the checksum with Ed25519, using the corresponding public key.
*
* @param string|resource|ReadOnlyFile $filename File name or file handle
* @param SignatureSecretKey $secretKey Secret key for digital signatures
* @param mixed $encoding Which encoding scheme to use for the signature?
* @return string Detached signature for the file
*
* @throws CannotPerformOperation
* @throws FileAccessDenied
* @throws FileError
* @throws InvalidKey
* @throws InvalidMessage
* @throws InvalidType
* @throws \TypeError
*/
public static function sign(
$filename,
SignatureSecretKey $secretKey,
$encoding = Halite::ENCODE_BASE64URLSAFE
): string {
if ($filename instanceof ReadOnlyFile) {
$pos = $filename->getPos();
$filename->reset(0);
$signature = self::signData(
$filename,
$secretKey,
$encoding
);
$filename->reset($pos);
return $signature;
}
if (\is_resource($filename) || \is_string($filename)) {
$readOnly = new ReadOnlyFile($filename);
try {
$signature = self::signData(
$readOnly,
$secretKey,
$encoding
);
return $signature;
} finally {
if (isset($readOnly)) {
$readOnly->close();
}
}
}
throw new InvalidType(
'Argument 1: Expected a filename or resource'
);
}
/**
* Verify a digital signature for a file.
*
* @param string|resource|ReadOnlyFile $filename File name or file handle
* @param SignaturePublicKey $publicKey Other party's signature public key
* @param string $signature The signature we received
* @param mixed $encoding Which encoding scheme to use for the signature?
*
* @return bool
* @throws CannotPerformOperation
* @throws FileAccessDenied
* @throws FileError
* @throws InvalidKey
* @throws InvalidMessage
* @throws InvalidSignature
* @throws InvalidType
* @throws \TypeError
*/
public static function verify(
$filename,
SignaturePublicKey $publicKey,
string $signature,
$encoding = Halite::ENCODE_BASE64URLSAFE
): bool {
if ($filename instanceof ReadOnlyFile) {
$pos = $filename->getPos();
$filename->reset(0);
$verified = self::verifyData(
$filename,
$publicKey,
$signature,
$encoding
);
$filename->reset($pos);
return $verified;
}
if (\is_resource($filename) || \is_string($filename)) {
$readOnly = new ReadOnlyFile($filename);
try {
$verified = self::verifyData(
$readOnly,
$publicKey,
$signature,
$encoding
);
return $verified;
} finally {
if (isset($readOnly)) {
$readOnly->close();
}
}
}
throw new InvalidType(
'Argument 1: Expected a filename or resource'
);
}
/**
* Calculate the BLAKE2b checksum of the contents of a file
*
* @param StreamInterface $fileStream
* @param Key $key
* @param mixed $encoding Which encoding scheme to use for the checksum?
* @return string
*
* @throws CannotPerformOperation
* @throws FileAccessDenied
* @throws FileError
* @throws InvalidKey
* @throws InvalidMessage
* @throws InvalidType
* @throws \TypeError
* @throws \SodiumException
*/
protected static function checksumData(
StreamInterface $fileStream,
Key $key = null,
$encoding = Halite::ENCODE_BASE64URLSAFE
): string {
$config = self::getConfig(
Halite::HALITE_VERSION_FILE,
'checksum'
);
// 1. Initialize the hash context
if ($key instanceof AuthenticationKey) {
// AuthenticationKey is for HMAC, but we can use it for keyed hashes too
$state = \sodium_crypto_generichash_init(
$key->getRawKeyMaterial(),
(int) $config->HASH_LEN
);
} elseif($config->CHECKSUM_PUBKEY && ($key instanceof SignaturePublicKey)) {
// In version 2, we use the public key as a hash key
$state = \sodium_crypto_generichash_init(
$key->getRawKeyMaterial(),
(int) $config->HASH_LEN
);
// @codeCoverageIgnoreStart
} elseif (isset($key)) {
// @codeCoverageIgnoreEnd
throw new InvalidKey(
'Argument 2: Expected an instance of AuthenticationKey or SignaturePublicKey'
);
} else {
$state = \sodium_crypto_generichash_init(
'',
(int) $config->HASH_LEN
);
}
// 2. Calculate the hash
$size = $fileStream->getSize();
while ($fileStream->remainingBytes() > 0) {
// Don't go past the file size even if $config->BUFFER is not an even multiple of it:
if (($fileStream->getPos() + (int) $config->BUFFER) > $size) {
/** @var int $amount_to_read */
$amount_to_read = ($size - $fileStream->getPos());
} else {
// @codeCoverageIgnoreStart
/** @var int $amount_to_read */
$amount_to_read = (int) $config->BUFFER;
// @codeCoverageIgnoreEnd
}
$read = $fileStream->readBytes($amount_to_read);
\sodium_crypto_generichash_update($state, $read);
}
// 3. Do we want a raw checksum?
$encoder = Halite::chooseEncoder($encoding);
if ($encoder) {
return (string) $encoder(
\sodium_crypto_generichash_final(
// @codeCoverageIgnoreStart
$state,
// @codeCoverageIgnoreEnd
(int) $config->HASH_LEN
)
);
}
return (string) \sodium_crypto_generichash_final(
// @codeCoverageIgnoreStart
$state,
// @codeCoverageIgnoreEnd
(int) $config->HASH_LEN
);
}
/**
* @param ReadOnlyFile $input
* @param MutableFile $output
* @param EncryptionKey $key
* @return int
*
* @throws CannotPerformOperation
* @throws FileAccessDenied
* @throws FileError
* @throws FileModified
* @throws InvalidDigestLength
* @throws InvalidKey
* @throws InvalidMessage
* @throws InvalidType
* @throws \TypeError
* @throws \SodiumException
*/
protected static function encryptData(
ReadOnlyFile $input,
MutableFile $output,
EncryptionKey $key
): int {
$config = self::getConfig(Halite::HALITE_VERSION_FILE, 'encrypt');
// Generate a nonce and HKDF salt
// @codeCoverageIgnoreStart
try {
$firstNonce = \random_bytes((int) $config->NONCE_BYTES);
$hkdfSalt = \random_bytes((int) $config->HKDF_SALT_LEN);
} catch (\Throwable $ex) {
throw new CannotPerformOperation($ex->getMessage());
}
// @codeCoverageIgnoreEnd
// Let's split our key
list ($encKey, $authKey) = self::splitKeys($key, $hkdfSalt, $config);
// Write the header
$output->writeBytes(
Halite::HALITE_VERSION_FILE,
Halite::VERSION_TAG_LEN
);
$output->writeBytes(
$firstNonce,
\SODIUM_CRYPTO_STREAM_NONCEBYTES
);
$output->writeBytes(
$hkdfSalt,
(int) $config->HKDF_SALT_LEN
);
// VERSION 2+ uses BMAC
$mac = \sodium_crypto_generichash_init($authKey);
\sodium_crypto_generichash_update($mac, Halite::HALITE_VERSION_FILE);
\sodium_crypto_generichash_update($mac, $firstNonce);
\sodium_crypto_generichash_update($mac, $hkdfSalt);
/** @var string $mac */
\sodium_memzero($authKey);
\sodium_memzero($hkdfSalt);
return self::streamEncrypt(
$input,
$output,
new EncryptionKey(
new HiddenString($encKey)
),
(string) $firstNonce,
(string) $mac,
$config
);
}
/**
* Decrypt the contents of a file.
*
* @param ReadOnlyFile $input
* @param MutableFile $output
* @param EncryptionKey $key
* @return bool
*
* @throws CannotPerformOperation
* @throws FileAccessDenied
* @throws FileError
* @throws FileModified
* @throws InvalidDigestLength
* @throws InvalidKey
* @throws InvalidMessage
* @throws InvalidType
* @throws \SodiumException
*/
protected static function decryptData(
ReadOnlyFile $input,
MutableFile $output,
EncryptionKey $key
): bool {
// Rewind
$input->reset(0);
// Make sure it's large enough to even read a version tag
if ($input->getSize() < Halite::VERSION_TAG_LEN) {
throw new InvalidMessage(
"Input file is too small to have been encrypted by Halite."
);
}
// Parse the header, ensuring we get 4 bytes
/** @var string $header */
$header = $input->readBytes(Halite::VERSION_TAG_LEN);
// Load the config
$config = self::getConfig($header, 'encrypt');
// Is this shorter than an encrypted empty string?
if ($input->getSize() < $config->SHORTEST_CIPHERTEXT_LENGTH) {
throw new InvalidMessage(
"Input file is too small to have been encrypted by Halite."
);
}
// Let's grab the first nonce and salt
/** @var string $firstNonce */
$firstNonce = $input->readBytes((int) $config->NONCE_BYTES);
/** @var string $hkdfSalt */
$hkdfSalt = $input->readBytes((int) $config->HKDF_SALT_LEN);
// Split our keys, begin the HMAC instance
list ($encKey, $authKey) = self::splitKeys($key, $hkdfSalt, $config);
// VERSION 2+ uses BMAC
$mac = \sodium_crypto_generichash_init($authKey);
\sodium_crypto_generichash_update($mac, $header);
\sodium_crypto_generichash_update($mac, $firstNonce);
\sodium_crypto_generichash_update($mac, $hkdfSalt);
/** @var string $mac */
$old_macs = self::streamVerify($input, Util::safeStrcpy($mac), $config);
\sodium_memzero($authKey);
\sodium_memzero($hkdfSalt);
$ret = self::streamDecrypt(
$input,
$output,
new EncryptionKey(
new HiddenString($encKey)
),
(string) $firstNonce,
(string) $mac,
$config,
$old_macs
);
\sodium_memzero($encKey);
unset($encKey);
unset($authKey);
unset($firstNonce);
unset($mac);
unset($config);
unset($old_macs);
return $ret;
}
/**
* Seal the contents of a file.
*
* @param ReadOnlyFile $input
* @param MutableFile $output
* @param EncryptionPublicKey $publicKey
* @return int
*
* @throws CannotPerformOperation
* @throws FileAccessDenied
* @throws FileModified
* @throws InvalidDigestLength
* @throws InvalidMessage
* @throws InvalidType
* @throws \Exception
* @throws \TypeError
*/
protected static function sealData(
ReadOnlyFile $input,
MutableFile $output,
EncryptionPublicKey $publicKey
): int {
// Generate a new keypair for this encryption
$ephemeralKeyPair = KeyFactory::generateEncryptionKeyPair();
$ephSecret = $ephemeralKeyPair->getSecretKey();
$ephPublic = $ephemeralKeyPair->getPublicKey();
unset($ephemeralKeyPair);
// Calculate the shared secret key
$sharedSecretKey = AsymmetricCrypto::getSharedSecret($ephSecret, $publicKey, true);
// @codeCoverageIgnoreStart
if (!($sharedSecretKey instanceof EncryptionKey)) {
throw new \TypeError('Shared secret is the wrong key type.');
}
// @codeCoverageIgnoreEnd
// Destroy the secret key after we have the shared secret
unset($ephSecret);
// Load the configuration
$config = self::getConfig(Halite::HALITE_VERSION_FILE, 'seal');
// Generate a nonce as per crypto_box_seal
$nonce = \sodium_crypto_generichash(
$ephPublic->getRawKeyMaterial() . $publicKey->getRawKeyMaterial(),
'',
\SODIUM_CRYPTO_STREAM_NONCEBYTES
);
// Generate a random HKDF salt
$hkdfSalt = \random_bytes((int) $config->HKDF_SALT_LEN);
// Split the keys
/**
* @var string $encKey
* @var string $authKey
*/
list ($encKey, $authKey) = self::splitKeys($sharedSecretKey, $hkdfSalt, $config);
// Write the header:
$output->writeBytes(
Halite::HALITE_VERSION_FILE,
Halite::VERSION_TAG_LEN
);
$output->writeBytes(
$ephPublic->getRawKeyMaterial(),
\SODIUM_CRYPTO_BOX_PUBLICKEYBYTES
);
$output->writeBytes(
$hkdfSalt,
(int) $config->HKDF_SALT_LEN
);
// VERSION 2+
$mac = \sodium_crypto_generichash_init($authKey);
// We no longer need $authKey after we set up the hash context
\sodium_memzero($authKey);
\sodium_crypto_generichash_update($mac, Halite::HALITE_VERSION_FILE);
\sodium_crypto_generichash_update($mac, $ephPublic->getRawKeyMaterial());
\sodium_crypto_generichash_update($mac, $hkdfSalt);
unset($ephPublic);
\sodium_memzero($hkdfSalt);
$ret = self::streamEncrypt(
$input,
$output,
new EncryptionKey(
new HiddenString($encKey)
),
(string) $nonce,
(string) $mac,
$config
);
\sodium_memzero($encKey);
unset($encKey);
unset($nonce);
return $ret;
}
/**
* Unseal the contents of a file.
*
* @param ReadOnlyFile $input
* @param MutableFile $output
* @param EncryptionSecretKey $secretKey
* @return bool
*
* @throws CannotPerformOperation
* @throws FileAccessDenied
* @throws FileError
* @throws FileModified
* @throws InvalidDigestLength
* @throws InvalidKey
* @throws InvalidMessage
* @throws InvalidType
* @throws \TypeError
*/
protected static function unsealData(
ReadOnlyFile $input,
MutableFile $output,
EncryptionSecretKey $secretKey
): bool {
$publicKey = $secretKey
->derivePublicKey();
// Is the file at least as long as a header?
if ($input->getSize() < Halite::VERSION_TAG_LEN) {
throw new InvalidMessage(
"Input file is too small to have been encrypted by Halite."
);
}
// Parse the header, ensuring we get 4 bytes
$header = $input->readBytes(Halite::VERSION_TAG_LEN);
// Load the config
$config = self::getConfig($header, 'seal');
if ($input->getSize() < $config->SHORTEST_CIPHERTEXT_LENGTH) {
throw new InvalidMessage(
"Input file is too small to have been encrypted by Halite."
);
}
// Let's grab the public key and salt
$ephPublic = $input->readBytes((int) $config->PUBLICKEY_BYTES);
$hkdfSalt = $input->readBytes((int) $config->HKDF_SALT_LEN);
// Generate the same nonce, as per sealData()
$nonce = \sodium_crypto_generichash(
$ephPublic . $publicKey->getRawKeyMaterial(),
'',
\SODIUM_CRYPTO_STREAM_NONCEBYTES
);
// Create a key object out of the public key:
$ephemeral = new EncryptionPublicKey(
new HiddenString($ephPublic)
);
$key = AsymmetricCrypto::getSharedSecret(
$secretKey,
$ephemeral,
true
);
// @codeCoverageIgnoreStart
if (!($key instanceof EncryptionKey)) {
throw new \TypeError();
}
// @codeCoverageIgnoreEnd
unset($ephemeral);
/**
* @var string $encKey
* @var string $authKey
*/
list ($encKey, $authKey) = self::splitKeys($key, $hkdfSalt, $config);
// We no longer need the original key after we split it
unset($key);
$mac = \sodium_crypto_generichash_init($authKey);
\sodium_crypto_generichash_update($mac, $header);
\sodium_crypto_generichash_update($mac, $ephPublic);
\sodium_crypto_generichash_update($mac, $hkdfSalt);
/** @var string $mac */
$oldMACs = self::streamVerify($input, Util::safeStrcpy($mac), $config);
// We no longer need these:
\sodium_memzero($authKey);
\sodium_memzero($hkdfSalt);
$ret = self::streamDecrypt(
$input,
$output,
new EncryptionKey(
new HiddenString($encKey)
),
$nonce,
$mac,
$config,
$oldMACs
);
\sodium_memzero($encKey);
unset($encKey);
unset($nonce);
unset($mac);
unset($config);
unset($oldMACs);
return $ret;
}
/**
* Sign the contents of a file
*
* @param ReadOnlyFile $input
* @param SignatureSecretKey $secretKey
* @param mixed $encoding Which encoding scheme to use for the signature?
* @return string
*
* @throws CannotPerformOperation
* @throws FileAccessDenied
* @throws FileError
* @throws InvalidKey
* @throws InvalidMessage
* @throws InvalidType
* @throws \TypeError
*/
protected static function signData(
ReadOnlyFile $input,
SignatureSecretKey $secretKey,
$encoding = Halite::ENCODE_BASE64URLSAFE
): string {
$checksum = self::checksumData(
$input,
$secretKey->derivePublicKey(),
true
);
return AsymmetricCrypto::sign(
$checksum,
$secretKey,
$encoding
);
}
/**
* Verify the contents of a file
*
* @param $input (file handle)
* @param SignaturePublicKey $publicKey
* @param string $signature
* @param mixed $encoding Which encoding scheme to use for the signature?
*
* @return bool
*
* @throws InvalidSignature
* @throws CannotPerformOperation
* @throws FileAccessDenied
* @throws FileError
* @throws InvalidKey
* @throws InvalidMessage
* @throws InvalidType
* @throws \TypeError
*/
protected static function verifyData(
ReadOnlyFile $input,
SignaturePublicKey $publicKey,
string $signature,
$encoding = Halite::ENCODE_BASE64URLSAFE
): bool {
$checksum = self::checksumData($input, $publicKey, true);
return AsymmetricCrypto::verify(
$checksum,
$publicKey,
$signature,
$encoding
);
}
/**
* Get the configuration
*
* @param string $header
* @param string $mode
* @return Config
* @throws InvalidMessage
* @throws InvalidType
*/
protected static function getConfig(
string $header,
string $mode = 'encrypt'
): Config {
if (\ord($header[0]) !== 49 || \ord($header[1]) !== 65) {
// @codeCoverageIgnoreStart
throw new InvalidMessage(
'Invalid version tag'
);
// @codeCoverageIgnoreEnd
}
$major = \ord($header[2]);
$minor = \ord($header[3]);
if ($mode === 'encrypt') {
return new Config(
self::getConfigEncrypt($major, $minor)
);
} elseif ($mode === 'seal') {
return new Config(
self::getConfigSeal($major, $minor)
);
} elseif ($mode === 'checksum') {
return new Config(
self::getConfigChecksum($major, $minor)
);
}
// @codeCoverageIgnoreStart
throw new InvalidType(
'Invalid configuration mode'
);
// @codeCoverageIgnoreEnd
}
/**
* Get the configuration for encrypt operations
*
* @param int $major
* @param int $minor
* @return array
* @throws InvalidMessage
*/
protected static function getConfigEncrypt(int $major, int $minor): array
{
if ($major === 4) {
return [
'SHORTEST_CIPHERTEXT_LENGTH' => 92,
'BUFFER' => 1048576,
'NONCE_BYTES' => \SODIUM_CRYPTO_STREAM_NONCEBYTES,
'HKDF_SALT_LEN' => 32,
'MAC_SIZE' => 32,
'HKDF_SBOX' => 'Halite|EncryptionKey',
'HKDF_AUTH' => 'AuthenticationKeyFor_|Halite'
];
} elseif ($major === 3) {
switch ($minor) {
case 0:
return [
'SHORTEST_CIPHERTEXT_LENGTH' => 92,
'BUFFER' => 1048576,
'NONCE_BYTES' => \SODIUM_CRYPTO_STREAM_NONCEBYTES,
'HKDF_SALT_LEN' => 32,
'MAC_SIZE' => 32,
'HKDF_SBOX' => 'Halite|EncryptionKey',
'HKDF_AUTH' => 'AuthenticationKeyFor_|Halite'
];
}
}
// If we reach here, we've got an invalid version tag:
// @codeCoverageIgnoreStart
throw new InvalidMessage(
'Invalid version tag'
);
// @codeCoverageIgnoreEnd
}
/**
* Get the configuration for seal operations
*
* @param int $major
* @param int $minor
* @return array
* @throws InvalidMessage
*/
protected static function getConfigSeal(int $major, int $minor): array
{
if ($major === 4) {
switch ($minor) {
case 0:
return [
'SHORTEST_CIPHERTEXT_LENGTH' => 100,
'BUFFER' => 1048576,
'HKDF_SALT_LEN' => 32,
'MAC_SIZE' => 32,
'PUBLICKEY_BYTES' => \SODIUM_CRYPTO_BOX_PUBLICKEYBYTES,
'HKDF_SBOX' => 'Halite|EncryptionKey',
'HKDF_AUTH' => 'AuthenticationKeyFor_|Halite'
];
}
} elseif ($major === 3) {
switch ($minor) {
case 0:
return [
'SHORTEST_CIPHERTEXT_LENGTH' => 100,
'BUFFER' => 1048576,
'HKDF_SALT_LEN' => 32,
'MAC_SIZE' => 32,
'PUBLICKEY_BYTES' => \SODIUM_CRYPTO_BOX_PUBLICKEYBYTES,
'HKDF_SBOX' => 'Halite|EncryptionKey',
'HKDF_AUTH' => 'AuthenticationKeyFor_|Halite'
];
}
}
// @codeCoverageIgnoreStart
throw new InvalidMessage(
'Invalid version tag'
);
// @codeCoverageIgnoreEnd
}
/**
* Get the configuration for encrypt operations
*
* @param int $major
* @param int $minor
* @return array
* @throws InvalidMessage
*/
protected static function getConfigChecksum(int $major, int $minor): array
{
if ($major === 3 || $major === 4) {
switch ($minor) {
case 0:
return [
'CHECKSUM_PUBKEY' => true,
'BUFFER' => 1048576,
'HASH_LEN' => \SODIUM_CRYPTO_GENERICHASH_BYTES_MAX
];
}
}
// @codeCoverageIgnoreStart
throw new InvalidMessage(
'Invalid version tag'
);
// @codeCoverageIgnoreEnd
}
/**
* Split a key using HKDF-BLAKE2b
*
* @param Key $master
* @param string $salt
* @param Config $config
* @return array
*
* @throws InvalidDigestLength
* @throws CannotPerformOperation
* @throws \TypeError
*/
protected static function splitKeys(
Key $master,
string $salt,
Config $config
): array {
$binary = $master->getRawKeyMaterial();
return [
Util::hkdfBlake2b(
$binary,
\SODIUM_CRYPTO_SECRETBOX_KEYBYTES,
(string) $config->HKDF_SBOX,
$salt
),
Util::hkdfBlake2b(
$binary,
\SODIUM_CRYPTO_AUTH_KEYBYTES,
(string) $config->HKDF_AUTH,
$salt
)
];
}
/**
* Stream encryption - Do not call directly
*
* @param ReadOnlyFile $input
* @param MutableFile $output
* @param EncryptionKey $encKey
* @param string $nonce
* @param string $mac (hash context for BLAKE2b)
* @param Config $config
*
* @return int (number of bytes)
*
* @throws FileError
* @throws CannotPerformOperation
* @throws FileAccessDenied
* @throws FileModified
* @throws \SodiumException
* @throws \TypeError
*/
final private static function streamEncrypt(
ReadOnlyFile $input,
MutableFile $output,
EncryptionKey $encKey,
string $nonce,
string $mac,
Config $config
): int {
$initHash = $input->getHash();
// Begin the streaming decryption
$size = $input->getSize();
$written = 0;
while ($input->remainingBytes() > 0) {
$read = $input->readBytes(
($input->getPos() + (int) $config->BUFFER) > $size
? ($size - $input->getPos())
: (int) $config->BUFFER
);
$encrypted = \sodium_crypto_stream_xor(
$read,
(string) $nonce,
$encKey->getRawKeyMaterial()
);
\sodium_crypto_generichash_update($mac, $encrypted);
$written += $output->writeBytes($encrypted);
\sodium_increment($nonce);
}
if (\is_string($nonce)) {
\sodium_memzero($nonce);
}
// Check that our input file was not modified before we MAC it
if (!\hash_equals($input->getHash(), $initHash)) {
// @codeCoverageIgnoreStart
throw new FileModified(
'Read-only file has been modified since it was opened for reading'
);
// @codeCoverageIgnoreEnd
}
$written += $output->writeBytes(
\sodium_crypto_generichash_final($mac, (int) $config->MAC_SIZE),
(int) $config->MAC_SIZE
);
return $written;
}
/**
* Stream decryption - Do not call directly
*
* @param ReadOnlyFile $input
* @param MutableFile $output
* @param EncryptionKey $encKey
* @param string $nonce
* @param string $mac (hash context for BLAKE2b)
* @param Config $config
* @param array &$chunk_macs
*
* @return bool
*
* @throws FileError
* @throws CannotPerformOperation
* @throws FileAccessDenied
* @throws FileModified
* @throws InvalidMessage
* @throws \TypeError
* @throws \SodiumException
*/
final private static function streamDecrypt(
ReadOnlyFile $input,
MutableFile $output,
EncryptionKey $encKey,
string $nonce,
string $mac,
Config $config,
array &$chunk_macs
): bool {
$start = $input->getPos();
/** @var int $cipher_end */
$cipher_end = $input->getSize() - (int) $config->MAC_SIZE;
// Begin the streaming decryption
$input->reset($start);
while ($input->remainingBytes() > (int) $config->MAC_SIZE) {
/**
* Would a full BUFFER read put it past the end of the
* ciphertext? If so, only return a portion of the file.
*/
if (($input->getPos() + (int) $config->BUFFER) > $cipher_end) {
$read = $input->readBytes(
$cipher_end - $input->getPos()
);
} else {
// @codeCoverageIgnoreStart
$read = $input->readBytes((int) $config->BUFFER);
// @codeCoverageIgnoreEnd
}
// Version 2+ uses a keyed BLAKE2b hash instead of HMAC
\sodium_crypto_generichash_update($mac, $read);
/** @var string $mac */
$calcMAC = Util::safeStrcpy($mac);
$calc = \sodium_crypto_generichash_final($calcMAC, (int) $config->MAC_SIZE);
if (empty($chunk_macs)) {
// @codeCoverageIgnoreStart
// Someone attempted to add a chunk at the end.
throw new InvalidMessage(
'Invalid message authentication code'
);
// @codeCoverageIgnoreEnd
} else {
/** @var string $chunkMAC */
$chunkMAC = \array_shift($chunk_macs);
if (!\hash_equals($chunkMAC, $calc)) {
// This chunk was altered after the original MAC was verified
// @codeCoverageIgnoreStart
throw new InvalidMessage(
'Invalid message authentication code'
);
// @codeCoverageIgnoreEnd
}
}
// This is where the decryption actually occurs:
$decrypted = \sodium_crypto_stream_xor(
$read,
(string) $nonce,
$encKey->getRawKeyMaterial()
);
$output->writeBytes($decrypted);
\sodium_increment($nonce);
}
if (\is_string($nonce)) {
\sodium_memzero($nonce);
}
return true;
}
/**
* Recalculate and verify the HMAC of the input file
*
* @param ReadOnlyFile $input The file we are verifying
* @param string $mac (hash context)
* @param Config $config Version-specific settings
*
* @return array Hashes of various chunks
*
* @throws CannotPerformOperation
* @throws FileAccessDenied
* @throws FileModified
* @throws InvalidMessage
* @throws \TypeError
* @throws \SodiumException
*/
final private static function streamVerify(
ReadOnlyFile $input,
$mac,
Config $config
): array {
/** @var int $start */
$start = $input->getPos();
// Grab the stored MAC:
/** @var int $cipher_end */
$cipher_end = $input->getSize() - (int) $config->MAC_SIZE;
$input->reset($cipher_end);
$stored_mac = $input->readBytes((int) $config->MAC_SIZE);
$input->reset($start);
$chunkMACs = [];
$break = false;
while (!$break && $input->getPos() < $cipher_end) {
/**
* Would a full BUFFER read put it past the end of the
* ciphertext? If so, only return a portion of the file.
*/
if (($input->getPos() + (int) $config->BUFFER) >= $cipher_end) {
$break = true;
$read = $input->readBytes($cipher_end - $input->getPos());
} else {
// @codeCoverageIgnoreStart
$read = $input->readBytes((int) $config->BUFFER);
// @codeCoverageIgnoreEnd
}
/**
* We're updating our HMAC and nothing else
*/
\sodium_crypto_generichash_update($mac, $read);
$mac = (string) $mac;
// Copy the hash state then store the MAC of this chunk
/** @var string $chunkMAC */
$chunkMAC = Util::safeStrcpy($mac);
$chunkMACs []= \sodium_crypto_generichash_final(
// @codeCoverageIgnoreStart
$chunkMAC,
// @codeCoverageIgnoreEnd
(int) $config->MAC_SIZE
);
}
/**
* We should now have enough data to generate an identical MAC
*/
$finalHMAC = \sodium_crypto_generichash_final(
// @codeCoverageIgnoreStart
$mac,
// @codeCoverageIgnoreEnd
(int) $config->MAC_SIZE
);
/**
* Use hash_equals() to be timing-invariant
*/
if (!\hash_equals($finalHMAC, $stored_mac)) {
throw new InvalidMessage(
'Invalid message authentication code'
);
}
$input->reset($start);
return $chunkMACs;
}
}
src/HiddenString.php 0000666 00000000172 13536431632 0010440 0 ustar 00 keyMaterial = Util::safeStrcpy($keyMaterial->getString());
}
/**
* Hide this from var_dump(), etc.
*
* @return array
* @codeCoverageIgnore
*/
public function __debugInfo()
{
// We exclude $this->keyMaterial
return [
'isAsymmetricKey' => $this->isAsymmetricKey,
'isPublicKey' => $this->isPublicKey,
'isSigningKey' => $this->isSigningKey
];
}
/**
* Make sure you wipe the key from memory on destruction
*/
public function __destruct()
{
if (!$this->isPublicKey) {
\sodium_memzero($this->keyMaterial);
$this->keyMaterial = '';
}
}
/**
* Don't allow this object to ever be serialized
* @throws CannotSerializeKey
* @codeCoverageIgnore
*/
public function __sleep()
{
throw new CannotSerializeKey;
}
/**
* Don't allow this object to ever be unserialized
* @throws CannotSerializeKey
* @codeCoverageIgnore
*/
public function __wakeup()
{
throw new CannotSerializeKey;
}
/**
* Get public keys
*
* @return string
* @codeCoverageIgnore
*/
public function __toString()
{
if ($this->isPublicKey) {
return $this->keyMaterial;
}
return '';
}
/**
* Get the actual key material
*
* @return string
* @throws \TypeError
*/
public function getRawKeyMaterial(): string
{
return Util::safeStrcpy($this->keyMaterial);
}
/**
* Is this a part of a key pair?
*
* @return bool
*/
public function isAsymmetricKey(): bool
{
return $this->isAsymmetricKey;
}
/**
* Is this a signing key?
*
* @return bool
*/
public function isEncryptionKey(): bool
{
return !$this->isSigningKey;
}
/**
* Is this a public key?
*
* @return bool
*/
public function isPublicKey(): bool
{
return $this->isPublicKey;
}
/**
* Is this a secret key?
*
* @return bool
*/
public function isSecretKey(): bool
{
return !$this->isPublicKey;
}
/**
* Is this a signing key?
*
* @return bool
*/
public function isSigningKey(): bool
{
return $this->isSigningKey;
}
}
doc/README.md 0000666 00000007325 13536431632 0006611 0 ustar 00 # Halite Documentation
1. [Basic Halite Usage](Basic.md)
2. [Halite Features](Features.md)
3. [Cryptography Primitives](Primitives.md)
4. [Class Documentation](Classes/)
## Class Documentation
* [`Alerts`](Classes/Alerts) (Exceptions)
* [`\ParagonIE\Halite\Alerts\CannotCloneKey`](Classes/Alerts/CannotCloneKey.md)
* [`\ParagonIE\Halite\Alerts\CannotPerformOperation`](Classes/Alerts/CannotPerformOperation.md)
* [`\ParagonIE\Halite\Alerts\CannotSerializeKey`](Classes/Alerts/CannotSerializeKey.md)
* [`\ParagonIE\Halite\Alerts\ConfigDirectiveNotFound`](Classes/Alerts/ConfigDirectiveNotFound.md)
* [`\ParagonIE\Halite\Alerts\FileAccessDenied`](Classes/Alerts/FileAccessDenied.md)
* [`\ParagonIE\Halite\Alerts\FileModified`](Classes/Alerts/FileModified.md)
* [`\ParagonIE\Halite\Alerts\HaliteAlert`](Classes/Alerts/HaliteAlert.md) (Base Exception for all Alerts)
* [`\ParagonIE\Halite\Alerts\InvalidDigestLength`](Classes/Alerts/InvalidDigestLength.md)
* [`\ParagonIE\Halite\Alerts\InvalidFlags`](Classes/Alerts/InvalidFlags.md)
* [`\ParagonIE\Halite\Alerts\InvalidKey`](Classes/Alerts/InvalidKey.md)
* [`\ParagonIE\Halite\Alerts\InvalidMessage`](Classes/Alerts/InvalidMessage.md)
* [`\ParagonIE\Halite\Alerts\InvalidSalt`](Classes/Alerts/InvalidSalt.md)
* [`\ParagonIE\Halite\Alerts\InvalidSignature`](Classes/Alerts/InvalidSignature.md)
* [`\ParagonIE\Halite\Alerts\InvalidType`](Classes/Alerts/InvalidType.md)
* [`Asymmetric`](Classes/Asymmetric)
* [`\ParagonIE\Halite\Asymmetric\Crypto`](Classes/Asymmetric/Crypto.md)
* [`\ParagonIE\Halite\Asymmetric\EncryptionPublicKey`](Classes/Asymmetric/EncryptionPublicKey.md)
* [`\ParagonIE\Halite\Asymmetric\EncryptionSecretKey`](Classes/Asymmetric/EncryptionSecretKey.md)
* [`\ParagonIE\Halite\Asymmetric\PublicKey`](Classes/Asymmetric/PublicKey.md)
* [`\ParagonIE\Halite\Asymmetric\SecretKey`](Classes/Asymmetric/SecretKey.md)
* [`\ParagonIE\Halite\Asymmetric\SignaturePublicKey`](Classes/Asymmetric/SignaturePublicKey.md)
* [`\ParagonIE\Halite\Asymmetric\SignatureSecretKey`](Classes/Asymmetric/SignatureSecretKey.md)
* [`Contract`](Classes/Contract) (Interfaces)
* [`\ParagonIE\Halite\Contract\StreamInterface`](Classes/Contract/StreamInterface.md)
* [`Stream`](Classes/Stream)
* [`\ParagonIE\Halite\Stream\MutableFile`](Classes/Stream/MutableFile.md)
* [`\ParagonIE\Halite\Stream\ReadOnlyFile`](Classes/Stream/ReadOnlyFile.md)
* [`\ParagonIE\Halite\Stream\WeakReadOnlyFile`](Classes/Stream/WeakReadOnlyFile.md)
* [`Structure`](Classes/Classes/Structure)
* [`\ParagonIE\Halite\Structure\BlockChain`](Classes/Stream/BlockChain.md)
* [`\ParagonIE\Halite\Structure\MerkleTree`](Classes/Stream/MerkleTree.md)
* [`\ParagonIE\Halite\Structure\Node`](Classes/Stream/Node.md)
* [`Symmetric`](Classes/Symmetric)
* [`\ParagonIE\Halite\Symmetric\AuthenticationKey`](Classes/Symmetric/AuthenticationKey.md)
* [`\ParagonIE\Halite\Symmetric\Config`](Classes/Symmetric/Config.md)
* [`\ParagonIE\Halite\Symmetric\Crypto`](Classes/Symmetric/Crypto.md)
* [`\ParagonIE\Halite\Symmetric\EncryptionKey`](Classes/Symmetric/EncryptionKey.md)
* [`\ParagonIE\Halite\Symmetric\SecretKey`](Classes/Symmetric/SecretKey.md)
* [`\ParagonIE\Halite\Config`](Config.md)
* [`\ParagonIE\Halite\Cookie`](Classes/Cookie.md)
* [`\ParagonIE\Halite\EncryptionKeyPair`](Classes/EncryptionKeyPair.md)
* [`\ParagonIE\Halite\File`](Classes/File.md)
* [`\ParagonIE\Halite\Halite`](Classes/Halite.md)
* [`\ParagonIE\Halite\Key`](Classes/Key.md)
* [`\ParagonIE\Halite\KeyFactory`](Classes/KeyFactory.md)
* [`\ParagonIE\Halite\KeyPair`](Classes/KeyPair.md)
* [`\ParagonIE\Halite\Password`](Classes/Password.md)
* [`\ParagonIE\Halite\SignatureKeyPair`](Classes/SignatureKeyPair.md)
* [`\ParagonIE\Halite\Util`](Classes/Util.md)
doc/Primitives.md 0000666 00000002332 13536431632 0010000 0 ustar 00 # Cryptography Primitives used in Halite
* Symmetric-key encryption: [**XSalsa20**](https://paragonie.com/book/pecl-libsodium/read/08-advanced.md#crypto-stream) (note: only [authenticated encryption](https://tonyarcieri.com/all-the-crypto-code-youve-ever-written-is-probably-broken) is available through Halite)
* Symmetric-key authentication: **[BLAKE2b](https://download.libsodium.org/doc/hashing/generic_hashing.html#singlepart-example-with-a-key)** (keyed)
* Asymmetric-key encryption: [**X25519**](https://paragonie.com/book/pecl-libsodium/read/08-advanced.md#crypto-scalarmult) followed by symmetric-key authenticated encryption
* Asymmetric-key digital signatures: [**Ed25519**](https://paragonie.com/book/pecl-libsodium/read/05-publickey-crypto.md#crypto-sign)
* Checksums: [**BLAKE2b**](https://paragonie.com/book/pecl-libsodium/read/06-hashing.md#crypto-generichash)
* Key splitting: [**HKDF-BLAKE2b**](Classes/Util.md)
* Password-Based Key Derivation: [**Argon2**](https://paragonie.com/book/pecl-libsodium/read/07-password-hashing.md#crypto-pwhash-str)
In all cases, we follow an Encrypt then MAC construction, thus avoiding the [cryptographic doom principle](http://www.thoughtcrime.org/blog/the-cryptographic-doom-principle).
doc/Classes/Cookie.md 0000666 00000001252 13536431632 0010453 0 ustar 00 # Cookie
**Namespace**: `\ParagonIE\Halite`
Encrypted cookie storage, powered by our [symmetric-key cryptography](Symmetric/Crypto.md).
## Properties
### protected `$key`
Stores the encryption key for this instance of `Cookie`.
## Methods
### Constructor
Arguments:
* [`EncryptionKey $key`](Symmetric/EncryptionKey.md) - The key used for symmetric-key encryption
### `fetch()`
> `public` fetch(`string $name`)
Fetch the data stored in an encrypted cookie.
### `store()`
> `public` store(`string $name`, `mixed $value`, `int $expire = 0`, `string $path = '/'`, `string $domain = null`, `boolean $secure = true`, `boolean $httponly = true`)
Encrypt then store a cookie. doc/Classes/KeyFactory.md 0000666 00000005724 13536431632 0011332 0 ustar 00 # KeyFactory (abstract)
**Namespace**: `\ParagonIE\Halite`
A factory class responsible for the creation and persistence of cryptography
keys.
## Methods
### `generateAuthenticationKey()`
> `public static` generateAuthenticationKey(`&$secret_key = null`): [`AuthenticationKey`](Symmetric/AuthenticationKey.md)
Generate an authentication key (symmetric-key cryptography).
### `generateEncryptionKey()`
> `public static` generateEncryptionKey(`&$secret_key = null`): [`EncryptionKey`](Symmetric/EncryptionKey.md)
Generate an encryption key (symmetric-key cryptography).
### `generateEncryptionKeyPair()`
> `public static` generateEncryptionKeyPair(`&$secret_key = null`): [`EncryptionKeyPair`](EncryptionKeyPair.md)
Generate a key pair for public key encryption.
### `generateSignatureKeyPair()`
> `public static` generateSignatureKeyPair(`&$secret_key = null`): [`SignatureKeyPair`](SignatureKeyPair.md)
Generate a key pair for public key digital signatures.
### `deriveAuthenticationKey()`
> `public static` deriveAuthenticationKey(`HiddenString $password`, `string $salt`, `string $level`): [`AuthenticationKey`](Symmetric/AuthenticationKey.md)
Derive a symmetric authentication key from a password and salt.
Acceptable values for `$level`:
* `KeyFactory::INTERACTIVE` - default
* `KeyFactory::MODERATE` - takes one to two seconds (depending on hardware)
* `KeyFactory::SENSITIVE` - takes several seconds; recommended for mission critical cryptography keys
### `deriveEncryptionKey()`
> `public static` deriveEncryptionKey(`HiddenString $password`, `string $salt`, `string $level`): [`EncryptionKey`](Symmetric/EncryptionKey.md)
Derive a symmetric encryption key from a password and salt.
### `deriveEncryptionKeyPair()`
> `public static` deriveEncryptionKeyPair(`HiddenString $password`, `string $salt`, `string $level`): [`EncryptionKeyPair`](EncryptionKeyPair.md)
Derive an asymmetric encryption key pair from a password and salt.
### `deriveSignatureKeyPair()`
> `public static` deriveSignatureKeyPair(`HiddenString $password`, `string $salt`, `string $level`): [`SignatureKeyPair`](SignatureKeyPair.md)
Derive an asymmetric signature key pair from a password and salt.
### `loadAuthenticationKey()`
> `public static` loadAuthenticationKey(`string $filePath`): [`AuthenticationKey`](Symmetric/AuthenticationKey.md)
Load an `AuthenticationKey` from a file.
### `loadEncryptionKey()`
> `public static` loadEncryptionKey(`string $filePath`): [`EncryptionKey`](Symmetric/EncryptionKey.md)
Load an `EncryptionKey` from a file.
### `loadEncryptionKeyPair()`
> `public static` loadEncryptionKeyPair(`string $filePath`): [`EncryptionKeyPair`](EncryptionKeyPair.md)
Load an `EncryptionKeyPair` from a file.
### `loadSignatureKeyPair()`
> `public static` loadSignatureKeyPair(`string $filePath`): [`SignatureKeyPair`](SignatureKeyPair.md)
Load an `SignatureKeyPair` from a file.
### `save()`
> `public static` save(`Key|KeyPair $key`, `string $filename = ''`)
Save a key to a file.
doc/Classes/File.md 0000666 00000006566 13536431632 0010136 0 ustar 00 # File
**Namespace**: `\ParagonIE\Halite`
## Methods
### `checksum()`
> `public static` checksum(`$filepath`, [`Key`](Key.md) `$key = null`, `$raw = false`) : `string`
Calculates a BLAKE2b-512 hash of the given file.
* `$filepath` - Path to a file (or an open file handle)
* `$key` (optional, should be an [`AuthenticationKey`](Symmetric/AuthenticationKey.md) or [`SignaturePublicKey`](Asymmetric/SignaturePublicKey.md))
* `$raw` - Set to `TRUE` if you don't want a hexadecimal string returned
### `encrypt()`
> `public static` encrypt(`$input`, `$output`, [`EncryptionKey`](Symmetric/EncryptionKey.md) `$key`): `string`
Encrypt the contents of `$input` (either a string containing the path to a file, or an open file
handle), and store it in the file (handle?) at `$output`.
Both `$input` and `$output` can be a string, a resource, or an object whose class implements `StreamInterface`.
In the object case, `$input` must be an instance of [`ReadOnlyFile`](Stream/ReadOnlyFile.md) and `$output` must
be an instance of [`MutableFile`](Stream/MutableFile.md).
### `decrypt()`
> `public static` decrypt(`$input`, `$output`, [`EncryptionKey`](Symmetric/EncryptionKey.md) `$key`): `string`
Decrypt the contents of `$input` (either a string containing the path to a file, or an open file
handle), and store it in the file (handle?) at `$output`.
Both `$input` and `$output` can be a string, a resource, or an object whose class implements `StreamInterface`.
In the object case, `$input` must be an instance of [`ReadOnlyFile`](Stream/ReadOnlyFile.md) and `$output` must
be an instance of [`MutableFile`](Stream/MutableFile.md).
### `seal()`
> `public static` seal(`$input`, `$output`, [`EncryptionPublicKey`](Asymmetric/EncryptionPublicKey.md) `$key`): `string`
Seals (encrypts with a public key) the contents of `$input` (either a string containing the path to a file, or an open file
handle), and store it in the file (handle?) at `$output`.
Both `$input` and `$output` can be a string, a resource, or an object whose class implements `StreamInterface`.
In the object case, `$input` must be an instance of [`ReadOnlyFile`](Stream/ReadOnlyFile.md) and `$output` must
be an instance of [`MutableFile`](Stream/MutableFile.md).
### `unseal()`
> `public static` unseal(`$input`, `$output`, [`EncryptionSecretKey`](Asymmetric/EncryptionSecretKey.md) `$key`) : `string`
Unseals (decrypts with a secret key) the contents of `$input` (either a string containing the path to a file, or an open file
handle), and store it in the file (handle?) at `$output`.
Both `$input` and `$output` can be a string, a resource, or an object whose class implements `StreamInterface`.
In the object case, `$input` must be an instance of [`ReadOnlyFile`](Stream/ReadOnlyFile.md) and `$output` must
be an instance of [`MutableFile`](Stream/MutableFile.md).
### `sign()`
> `public static` sign(`$input`, [`SignatureSecretKey`](Asymmetric/SignatureSecretKey.md) `$key`, `bool $raw_binary`): `string`
Calculate a digital signature of a file.
`$input` can be a string or a resource, or an instance of [`ReadOnlyFile`](Stream/ReadOnlyFile.md).
### `verify()`
> `public static` sign(`$input`, [`SignaturePublicKey`](Asymmetric/SignaturePublicKey.md) `$key`, `string $signature`, `boolean $raw_binary`): `bool`
Verifies a digital signature of a file.
`$input` can be a string or a resource, or an instance of [`ReadOnlyFile`](Stream/ReadOnlyFile.md).
doc/Classes/Password.md 0000666 00000001324 13536431632 0011044 0 ustar 00 # Password (abstract)
**Namespace**: `\ParagonIE\Halite`
A simplified interface for storing encrypted password hashes (hash then encrypt)
for user authentication, powered by our [symmetric-key cryptography](Symmetric/Crypto.md).
## Static Methods
### `hash()`
> `public static` hash(`HiddenString $password`, `EncryptionKey $secret_key`): `HiddenString`
Hash a password (with a randomly generated Argon2 salt), then encrypt the hash
using our [symmetric encryption key](Symmetric/EncryptionKey.md).
### `verify()`
> `public static` verify(`HiddenString $password`, `HiddenString $stored`, `EncryptionKey $secret_key`): `bool`
Decrypt the `$stored` password hash, then verify that it matches the given
`$password`.
doc/Classes/README.md 0000666 00000006371 13536431632 0010206 0 ustar 00 ## Class Documentation
* [`Alerts`](Alerts) (Exceptions)
* [`\ParagonIE\Halite\Alerts\CannotCloneKey`](Alerts/CannotCloneKey.md)
* [`\ParagonIE\Halite\Alerts\CannotPerformOperation`](Alerts/CannotPerformOperation.md)
* [`\ParagonIE\Halite\Alerts\CannotSerializeKey`](Alerts/CannotSerializeKey.md)
* [`\ParagonIE\Halite\Alerts\ConfigDirectiveNotFound`](Alerts/ConfigDirectiveNotFound.md)
* [`\ParagonIE\Halite\Alerts\FileAccessDenied`](Alerts/FileAccessDenied.md)
* [`\ParagonIE\Halite\Alerts\FileModified`](Alerts/FileModified.md)
* [`\ParagonIE\Halite\Alerts\HaliteAlert`](Alerts/HaliteAlert.md) (Base Exception for all Alerts)
* [`\ParagonIE\Halite\Alerts\InvalidDigestLength`](Alerts/InvalidDigestLength.md)
* [`\ParagonIE\Halite\Alerts\InvalidFlags`](Alerts/InvalidFlags.md)
* [`\ParagonIE\Halite\Alerts\InvalidKey`](Alerts/InvalidKey.md)
* [`\ParagonIE\Halite\Alerts\InvalidMessage`](Alerts/InvalidMessage.md)
* [`\ParagonIE\Halite\Alerts\InvalidSalt`](Alerts/InvalidSalt.md)
* [`\ParagonIE\Halite\Alerts\InvalidSignature`](Alerts/InvalidSignature.md)
* [`\ParagonIE\Halite\Alerts\InvalidType`](Alerts/InvalidType.md)
* [`Asymmetric`](Asymmetric)
* [`\ParagonIE\Halite\Asymmetric\Crypto`](Asymmetric/Crypto.md)
* [`\ParagonIE\Halite\Asymmetric\EncryptionPublicKey`](Asymmetric/EncryptionPublicKey.md)
* [`\ParagonIE\Halite\Asymmetric\EncryptionSecretKey`](Asymmetric/EncryptionSecretKey.md)
* [`\ParagonIE\Halite\Asymmetric\PublicKey`](Asymmetric/PublicKey.md)
* [`\ParagonIE\Halite\Asymmetric\SecretKey`](Asymmetric/SecretKey.md)
* [`\ParagonIE\Halite\Asymmetric\SignaturePublicKey`](Asymmetric/SignaturePublicKey.md)
* [`\ParagonIE\Halite\Asymmetric\SignatureSecretKey`](Asymmetric/SignatureSecretKey.md)
* [`Contract`](Contract) (Interfaces)
* [`\ParagonIE\Halite\Contract\StreamInterface`](Contract/StreamInterface.md)
* [`Stream`](Stream)
* [`\ParagonIE\Halite\Stream\MutableFile`](Stream/MutableFile.md)
* [`\ParagonIE\Halite\Stream\ReadOnlyFile`](Stream/ReadOnlyFile.md)
* [`\ParagonIE\Halite\Stream\WeakReadOnlyFile`](Stream/WeakReadOnlyFile.md)
* [`Structure`](Classes/Structure)
* [`\ParagonIE\Halite\Structure\BlockChain`](Stream/BlockChain.md)
* [`\ParagonIE\Halite\Structure\MerkleTree`](Stream/MerkleTree.md)
* [`\ParagonIE\Halite\Structure\Node`](Stream/Node.md)
* [`Symmetric`](Symmetric)
* [`\ParagonIE\Halite\Symmetric\AuthenticationKey`](Symmetric/AuthenticationKey.md)
* [`\ParagonIE\Halite\Symmetric\Config`](Symmetric/Config.md)
* [`\ParagonIE\Halite\Symmetric\Crypto`](Symmetric/Crypto.md)
* [`\ParagonIE\Halite\Symmetric\EncryptionKey`](Symmetric/EncryptionKey.md)
* [`\ParagonIE\Halite\Symmetric\SecretKey`](Symmetric/SecretKey.md)
* [`\ParagonIE\Halite\Password`](Asymmetric.md)
* [`\ParagonIE\Halite\Cookie`](Cookie.md)
* [`\ParagonIE\Halite\EncryptionKeyPair`](EncryptionKeyPair.md)
* [`\ParagonIE\Halite\File`](File.md)
* [`\ParagonIE\Halite\Halite`](Halite.md)
* [`\ParagonIE\Halite\Key`](Key.md)
* [`\ParagonIE\Halite\KeyFactory`](KeyFactory.md)
* [`\ParagonIE\Halite\KeyPair`](KeyPair.md)
* [`\ParagonIE\Halite\Password`](Password.md)
* [`\ParagonIE\Halite\SignatureKeyPair`](SignatureKeyPair.md)
* [`\ParagonIE\Halite\Util`](Util.md) doc/Classes/Key.md 0000666 00000003147 13536431632 0007777 0 ustar 00 # Key
**Namespace**: `\ParagonIE\Halite`
## Constants
Flags:
```php
const SECRET_KEY = 1;
const PUBLIC_KEY = 2;
const ENCRYPTION = 4;
const SIGNATURE = 8;
const ASYMMETRIC = 16;
```
Alias Flags:
```php
const AUTHENTICATION = 8;
```
Shortcut flags:
```php
const CRYPTO_SECRETBOX = 5;
const CRYPTO_AUTH = 9;
const CRYPTO_BOX = 20;
const CRYPTO_SIGN = 24;
```
## Methods
### Constructor
**Warning**: You should almost never use `Key` directly. Instead, use one of the child classes.
Arguments:
* `$keyMaterial` - `HiddenString` containing raw binary data (the cryptographic key)
Example:
```php
// For Symmetric::encrypt()
$enc_secret = new EncryptionKey(
new HiddenString(str_repeat('A', 32))
);
// For Symmetric::authenticate()
$auth_secret = new AuthenticationKey(
new HiddenString(str_repeat('A', 32))
);
```
### `getRawKeyMaterial()`
> `public` getRawKeyMaterial()
Simply returns the raw binary key data.
### `isAsymmetricKey()`
>`public` isAsymmetricKey()
Returns true if this is a key meant for asymmetric cryptography.
### `isEncryptionKey()`
> `public` isEncryptionKey()
Returns true if this is a key meant for encryption.
### `isPublicKey()`
> `public` isPublicKey()
Returns true if this is the public key for a given key-pair.
### `isSecretKey()`
> `public` isSecretKey()
Returns true if:
* Symmetric crypto: Always
* Asymmetric crypto: This is the secret key for a given key-pair.
### `isSigningKey()`
> `public` isSigningKey()
Returns true if this is a key meant for authentication
doc/Classes/Util.md 0000666 00000003243 13536431632 0010161 0 ustar 00 # Util (abstract)
**Namespace**: `\ParagonIE\Halite`
## Static Methods
### `hash()`
> `public static` hash(`string $input`, `int $length = 32`): `string`
User-friendly wrapper for `sodium_crypto_generichash`.
Returns a hexadecimal-encoded hash of an input, for any length.
### `keyed_hash()`
> `public static` hash(`string $input`, `string $key`, `int $length = 32`): `string`
User-friendly wrapper for `sodium_crypto_generichash`.
Returns a hexadecimal-encoded keyed hash of an input, for any length.
### `raw_hash()`
> `public static` rawhash(`string $input`, `int $length = 32`): `string`
User-friendly wrapper for `sodium_crypto_generichash`.
Returns a raw binary hash of an input, for any length.
### `raw_keyed_hash()`
> `public static` hash(`string $input`, `string $key`, `int $length = 32`): `string`
User-friendly wrapper for `sodium_crypto_generichash`.
Returns a raw binary keyed hash of an input, for any length.
### `hkdfBlake2b()`
> `public static` hkdfBlake2b(`string $ikm`, `int $length`, `string $info = ''`, `string $salt = null`): `int`
This is a variant of HKDF-HMAC (RFC 5869). Instead of HMAC, it uses a keyed hash
function (BLAKE2b) for key splitting.
### `safeStrcpy()`
> `public static` safeStrcpy(`string $str`): `string`
Returns a copy of a string without triggering PHP's optimizations. The
string returned by this method can safely be used with `sodium_memzero()`
without corrupting other copies of the same string.
### `xorStrings()`
> `public static` xorStrings(`string $left`, `string $right`): `string`
Calculate A xor B, given two binary strings of the same length.
Uses pack() and unpack() to avoid cache-timing leaks caused by chr().
doc/Classes/Contract/StreamInterface.md 0000666 00000000112 13536431632 0014065 0 ustar 00 # StreamInterface (interface)
**Namespace**: `\ParagonIE\Halite\Contract` doc/Classes/Asymmetric/SignatureSecretKey.md 0000666 00000000372 13536431632 0015141 0 ustar 00 # SignatureSecretKey extends [SecretKey](SecretKey.md)
**Namespace**: `\ParagonIE\Halite\Asymmetric`
Represents an Ed25519 secret key.
* Asymmetric? **YES**
* Public key? no
* Secret key? **YES**
* Encryption key? no
* Authentication key? **YES**
doc/Classes/Asymmetric/EncryptionPublicKey.md 0000666 00000000375 13536431632 0015326 0 ustar 00 # EncryptionPublicKey extends [PublicKey](PublicKey.md)
**Namespace**: `\ParagonIE\Halite\Asymmetric`
Represents a Curve25519 public key.
* Asymmetric? **YES**
* Public key? **YES**
* Secret key? no
* Encryption key? **YES**
* Authentication key? no
doc/Classes/Asymmetric/SignaturePublicKey.md 0000666 00000000372 13536431632 0015132 0 ustar 00 # SignaturePublicKey extends [PublicKey](PublicKey.md)
**Namespace**: `\ParagonIE\Halite\Asymmetric`
Represents an Ed25519 secret key.
* Asymmetric? **YES**
* Public key? **YES**
* Secret key? no
* Encryption key? no
* Authentication key? **YES**
doc/Classes/Asymmetric/EncryptionSecretKey.md 0000666 00000000375 13536431632 0015335 0 ustar 00 # EncryptionSecretKey extends [SecretKey](SecretKey.md)
**Namespace**: `\ParagonIE\Halite\Asymmetric`
Represents a Curve25519 secret key.
* Asymmetric? **YES**
* Public key? no
* Secret key? **YES**
* Encryption key? **YES**
* Authentication key? no
doc/Classes/Asymmetric/PublicKey.md 0000666 00000000414 13536431632 0013245 0 ustar 00 # PublicKey extends [Key](../Key.md)
**Namespace**: `\ParagonIE\Halite\Asymmetric`
Represents an asymmetric public key (Curve25519 or Ed25519).
* Asymmetric? **YES**
* Public key? **YES**
* Secret key? no
* Encryption key? *unknown*
* Authentication key? *unknown*
doc/Classes/Asymmetric/SecretKey.md 0000666 00000000414 13536431632 0013254 0 ustar 00 # SecretKey extends [Key](../Key.md)
**Namespace**: `\ParagonIE\Halite\Asymmetric`
Represents an asymmetric secret key (Curve25519 or Ed25519).
* Asymmetric? **YES**
* Public key? no
* Secret key? **YES**
* Encryption key? *unknown*
* Authentication key? *unknown*
doc/Classes/Asymmetric/Crypto.md 0000666 00000012257 13536431632 0012646 0 ustar 00 # Crypto (abstract)
**Namespace**: `\ParagonIE\Halite\Asymmetric`
## Methods
### `getSharedSecret()`
> `public` getSharedSecret([`EncryptionSecretKey`](EncryptionSecretKey.md) `$privateKey`, [`EncryptionPublicKey`](EncryptionPublicKey.md) `$publicKey`, `$get_as_object = false`) : [`EncryptionKey`](../Symmetric/EncryptionKey.md)
This method calculates a shared [`EncryptionKey`](../Symmetric/EncryptionKey.md)
using X25519 (Elliptic Curve Diffie Hellman key agreement over Curve25519).
### `encrypt()`
> `public` encrypt(`HiddenString $source`, [`EncryptionSecretKey`](EncryptionSecretKey.md) `$ourPrivateKey`, [`EncryptionPublicKey`](EncryptionPublicKey.md) `$theirPublicKey`, `$encoding = Halite::ENCODE_BASE64URLSAFE`) : `string`
This method will:
1. Calculate a shared symmetric encryption key between your secret key and your
recipient's public key.
2. Generate a random HKDF salt.
3. Split the shared secret using salted HKDF.
4. Generate a random nonce.
5. Encrypt your plaintext (`$source`) with the derived encryption key (step 3).
6. MAC the ciphertext (step 5), along with the current library version, the HKDF
salt, and the nonce, with the derived authentication key (step 3).
7. Return the output of step 6 either as raw binary or as a hex-encoded string.
### `decrypt()`
> `public` decrypt(`string $source`, [`EncryptionSecretKey`](EncryptionSecretKey.md) `$ourPrivateKey`, [`EncryptionPublicKey`](EncryptionPublicKey.md) `$theirPublicKey`, `$encoding = Halite::ENCODE_BASE64URLSAFE`) : `HiddenString`
This method will:
1. If we aren't expecting raw data, we treat `$source` as a hex string and
decode it to raw binary.
2. Calculate a shared symmetric encryption key between your secret key and the
sender's public key.
3. Parse the library version tag, HKDF salt, and nonce from the message.
4. Split the shared secret using salted HKDF.
5. Verify the MAC using the derived authentication key (step 4).
6. If step 5 is successful, decrypt the ciphertext with the derived encryption
key (step 4).
7. Return what should be the original plaintext.
### `encryptWithAd()`
> `public` encryptWithAd(`HiddenString $plaintext`, [`EncryptionSecretKey`](EncryptionSecretKey.md) `$ourPrivateKey`, [`EncryptionPublicKey`](EncryptionPublicKey.md) `$theirPublicKey`, `string $additionalData = ''`, `$encoding = Halite::ENCODE_BASE64URLSAFE`): `string`
This is similar to `encrypt()`, except the `$additionalData` string is prepended to the ciphertext (after the nonce) when calculating the Message Authentication Code (MAC).
### `decryptWithAd()`
> `public` decryptWithAd(`string $ciphertext`, [`EncryptionSecretKey`](EncryptionSecretKey.md) `$ourPrivateKey`, [`EncryptionPublicKey`](EncryptionPublicKey.md) `$theirPublicKey`, `string $additionalData = ''`, `$encoding = Halite::ENCODE_BASE64URLSAFE`): `HiddenString`
This is similar to `decrypt()`, except the `$additionalData` string is prepended to the ciphertext (after the nonce) when calculating the Message Authentication Code (MAC).
### `seal()`
> `public` seal(`HiddenString $source`, [`EncryptionPublicKey`](EncryptionPublicKey.md) `$publicKey`, `$encoding = Halite::ENCODE_BASE64URLSAFE`) : `string`
Anonymous public-key encryption. Encrypt a message with your recipient's public
key and they can use their secret key to decrypt it.
The actual underlying protocol is [`sodium_crypto_box_seal()`](https://paragonie.com/book/pecl-libsodium/read/08-advanced.md#crypto-box-seal).
### `unseal()`
> `public` unseal(`string $source`, [`EncryptionSecretKey`](EncryptionSecretKey.md) `$secretKey`, `$encoding = Halite::ENCODE_BASE64URLSAFE`) : `HiddenString`
Anonymous public-key decryption. Decrypt a sealed message with your secret key.
The actual underlying protocol is [`sodium_crypto_box_seal_open()`](https://paragonie.com/book/pecl-libsodium/read/08-advanced.md#crypto-box-seal).
### `sign()`
> `public` sign(`string $message`, [`SignatureSecretKey`](SignatureSecretKey.md) `$secretKey`, `$encoding = Halite::ENCODE_BASE64URLSAFE`) : `string`
Calculates a digital signature of `$message`, using [`sodium_crypto_sign()`](https://paragonie.com/book/pecl-libsodium/read/05-publickey-crypto.md#crypto-sign).
### `verify()`
> `public` verify(`string $message`, [`SignaturePublicKey`](SignaturePublicKey.md) `$secretKey`, `string $signature`, `$encoding = Halite::ENCODE_BASE64URLSAFE`) : `boolean`
Does the signature match the contents of the message, for the given public key?
### `signAndEncrypt()`
> `public` signAndEncrypt(`HiddenString $message`, [`SignatureSecretKey`](SignatureSecretKey.md) `$secretKey`, [`PublicKey`](PublicKey.md) `$recipientPublicKey`, `$encoding = Halite::ENCODE_BASE64URLSAFE`) : `string`
Signs and encrypts a message. Note that a `SignaturePublicKey` or `EncryptionPublicKey`
is acceptable for the third argument. This is intended to facilitate the GPG use-case.
> `public` verifyAndDecrypt(`string $message`, [`SignaturePublicKey`](SignaturePublicKey.md) `$secretKey`, [`SecretKey`](SecretKey.md) `$mySecretKey`, `$encoding = Halite::ENCODE_BASE64URLSAFE`) : `HiddenString`
Decrypts and verifies a message. Note that a `SignatureSecretKey` or `EncryptionSecretKey`
is acceptable for the third argument. This is intended to facilitate the GPG use-case.
doc/Classes/Alerts/FileModified.md 0000666 00000000641 13536431632 0013015 0 ustar 00 # FileModified extends [HaliteAlert](HaliteAlert.md)
**Namespace**: `\ParagonIE\Halite\Alerts`
This indicates a race condition was being exploited against your app. This
happens when a file you were attempting to decrypt was modified after it was
opened for decryption.
There are a few possible causes to consider:
* Cloud storage apps (DropBox, Google Drive, etc.)
* Malware
* Filesystem bugs
* Hardware errors
doc/Classes/Alerts/InvalidType.md 0000666 00000000140 13536431632 0012717 0 ustar 00 # InvalidType extends [HaliteAlert](HaliteAlert.md)
**Namespace**: `\ParagonIE\Halite\Alerts`
doc/Classes/Alerts/InvalidDigestLength.md 0000666 00000000257 13536431632 0014370 0 ustar 00 # InvalidDigestLength extends [HaliteAlert](HaliteAlert.md)
**Namespace**: `\ParagonIE\Halite\Alerts`
Internal exception thrown by [our BLAKE2b HKDF derivative](../Util.md). doc/Classes/Alerts/FileAccessDenied.md 0000666 00000000420 13536431632 0013602 0 ustar 00 # FileAccessDenied extends [HaliteAlert](HaliteAlert.md)
**Namespace**: `\ParagonIE\Halite\Alerts`
Halite is unable to access the requested file. It might be read-only (if the
failure occurred with an output file), or there might be open_basedir
restrictions in effect. doc/Classes/Alerts/CannotPerformOperation.md 0000666 00000000477 13536431632 0015142 0 ustar 00 # CannotPerformOperation extends [HaliteAlert](HaliteAlert.md)
**Namespace**: `\ParagonIE\Halite\Alerts`
A generic internal error. This usually means some internal PHP function is
failing and it shouldn't be.
When reporting issues relating to this, please include the portion of the stack
trace relevant to Halite.
doc/Classes/Alerts/InvalidKey.md 0000666 00000000137 13536431632 0012534 0 ustar 00 # InvalidKey extends [HaliteAlert](HaliteAlert.md)
**Namespace**: `\ParagonIE\Halite\Alerts`
doc/Classes/Alerts/ConfigDirectiveNotFound.md 0000666 00000000442 13536431632 0015215 0 ustar 00 # ConfigDirectiveNotFound extends [HaliteAlert](HaliteAlert.md)
**Namespace**: `\ParagonIE\Halite\Alerts`
You should never run into this error. It means that the hard-coded configuration
directives were not found at run-time, and indicates a serious error in your
installation of Halite.
doc/Classes/Alerts/InvalidSalt.md 0000666 00000000137 13536431632 0012707 0 ustar 00 # InvalidSalt extends [HaliteAlert](HaliteAlert.md)
**Namespace**: `\ParagonIE\Halite\Alerts`
doc/Classes/Alerts/InvalidFlags.md 0000666 00000000520 13536431632 0013034 0 ustar 00 # InvalidFlags extends [HaliteAlert](HaliteAlert.md)
**Namespace**: `\ParagonIE\Halite\Alerts`
The flags passed to a constructor were wrong. If you are using the API as
directed (i.e. not ever touching `Key` or `KeyPair` directly and instead using
one of their derived classes), you shouldn't ever encounter this Alert being
thrown.
doc/Classes/Alerts/HaliteAlert.md 0000666 00000000557 13536431632 0012701 0 ustar 00 # HaliteAlert
**Namespace**: `\ParagonIE\Halite\Alerts`
This is the base class from which all of our custom Exception classes extend.
If you write code like this:
```php
try {
// Do something with Halite here...
} catch (\ParagonIE\Halite\Alerts\HaliteAlert $e) {
// Oh no!
}
```
...then you should catch every run-time exception this library will throw. doc/Classes/Alerts/CannotCloneKey.md 0000666 00000000432 13536431632 0013347 0 ustar 00 # CannotCloneKey extends [HaliteAlert](HaliteAlert.md)
**Namespace**: `\ParagonIE\Halite\Alerts`
All key objects should never permit this usage:
```php
$key = KeyFactory::generateEncryptionKey();
$cloned = clone $key;
```
If you attempt to do this, it will throw this exception. doc/Classes/Alerts/InvalidSignature.md 0000666 00000000144 13536431632 0013743 0 ustar 00 # InvalidSignature extends [HaliteAlert](HaliteAlert.md)
**Namespace**: `\ParagonIE\Halite\Alerts`
doc/Classes/Alerts/InvalidMessage.md 0000666 00000000143 13536431632 0013365 0 ustar 00 # InvalidMessage extends [HaliteAlert](HaliteAlert.md)
**Namespace**: `\ParagonIE\Halite\Alerts`
doc/Classes/Alerts/CannotSerializeKey.md 0000666 00000000563 13536431632 0014243 0 ustar 00 # CannotSerializeKey extends [HaliteAlert](HaliteAlert.md)
**Namespace**: `\ParagonIE\Halite\Alerts`
All key objects should never permit this usage:
```php
$key = KeyFactory::generateEncryptionKey();
$store = serialize($key);
```
If you attempt to do this, it will throw this exception.
Instead, use `KeyFactory::save($key, '/path/to/file');` for persistent storage. doc/Classes/EncryptionKeyPair.md 0000666 00000000663 13536431632 0012666 0 ustar 00 # EncryptionKeyPair extends [KeyPair](KeyPair.md)
**Namespace**: `\ParagonIE\Halite`
This [`KeyPair`](KeyPair.md) class represents a secret key and public key used for deriving a
shared secret using X25519, which is then used for symmetric-key encryption.
* Secret Key: [`EncryptionSecretKey`](Asymmetric/EncryptionSecretKey.md)
* Public Key: [`EncryptionPublicKey`](Asymmetric/EncryptionPublicKey.md)
## Methods
### Constructor
doc/Classes/Stream/ReadOnlyFile.md 0000666 00000003005 13536431632 0013010 0 ustar 00 # ReadOnlyFile
**Namespace**: `\ParagonIE\Halite\Stream`
This represents a file that we are reading from, which should never be altered
while our cryptography operations are being performed.
## Constants
```php
const ALLOWED_MODES = ['rb'];
// PHP's fread() buffer is set to 8192 by default
const CHUNK = 8192;
```
## Properties
* `$fp` (private) - File pointer
* `int $pos` (private) - Position within the stream (via `ftell()`)
* `array $stat` (private) - Statistics about the file (via `fstat()`)
## Methods
### Constructor
Arguments:
* `$file` - Either a string containing a file location or a resource (file
handle opened by `fopen()`)
### `getHash()`
> `public` getHash() : `string`
Returns the BLAKE2b hash of the file contents.
### `readBytes()`
> `public` readBytes(`int $num`) : `string`
Read the desired number of bytes from the internal stream, preventing partial
reads. Also performs runtime checks to prevent TOCTOU attacks (race conditions).
### `remainingBytes()`
> `public` remainingBytes() : `int`
Returns the number of bytes between the current location and the end of the
stream.
### `reset()`
> `public` reset(`int $i = 0`)
Set the current position in the stream to the desired value.
### `toctouTest()`
> `public` toctouTest()
Verifies that the file location (`ftell($this->fp)`) has not diverged from our
current location (`$this->loc`), and that the file size has not changed.
### `writeBytes()`
> `public `writeBytes(`string $buf`, `int $num = null`)
Just returns `false`. doc/Classes/Stream/WeakReadOnlyFile.md 0000666 00000000423 13536431632 0013621 0 ustar 00 # WeakReadOnlyFile extends [ReadOnlyFile](ReadOnlyFile.md)
**Namespace**: `\ParagonIE\Halite\Stream`
This is a more permissive alternative to `ReadOnlyFile`. Available since 4.4.0.
## Constants
```php
const ALLOWED_MODES = ['rb', 'r+b', 'wb', 'w+b', 'cb', 'c+b'];
``` doc/Classes/Stream/MutableFile.md 0000666 00000001757 13536431632 0012700 0 ustar 00 # MutableFile
**Namespace**: `\ParagonIE\Halite\Stream`
This represents a file that we are writing to, and therefore is mutable.
## Constants
```php
const ALLOWED_MODES = ['r+b', 'w+b', 'cb', 'c+b'];
// PHP's fread() buffer is set to 8192 by default
const CHUNK = 8192;
```
## Properties
* `$fp` (private) - File pointer
* `int $pos` (private) - Position within the stream (via `ftell()`)
* `array $stat` (private) - Statistics about the file (via `fstat()`)
## Methods
### Constructor
Arguments:
* `$file` - Either a string containing a file location or a resource (file
handle opened by `fopen()`)
### `readBytes()`
> `public` readBytes(`int $num`) : `string`
Read the desired number of bytes from the internal stream, preventing partial
reads.
### `reset()`
> `public` reset(`int $i = 0`)
Set the current position in the stream to the desired value.
### `writeBytes()`
> `public` writeBytes(`string $buf`, `int $num = null`) : `int`
Write `$buf` to the internal stream.
doc/Classes/Halite.md 0000666 00000002103 13536431632 0010444 0 ustar 00 # Halite
**Namespace**: `\ParagonIE\Halite`
This is just an abstract class that contains some constants for the current
release of Halite.
## Constants
const VERSION = '3.0.0';
const HALITE_VERSION_KEYS = "\x31\x40\x03\x00";
const HALITE_VERSION_FILE = "\x31\x41\x03\x00";
const HALITE_VERSION = "\x31\x42\x03\x00";
const VERSION_TAG_LEN = 4;
const VERSION_PREFIX = 'MUIDA';
const ENCODE_HEX = 'hex';
const ENCODE_BASE32 = 'base32';
const ENCODE_BASE32HEX = 'base32hex';
const ENCODE_BASE64 = 'base64';
const ENCODE_BASE64URLSAFE = 'base64urlsafe';
## Static Methods
### chooseEncoder()
> `public static` chooseEncoder(`$chosen`, `bool $decode = false`)
Used to determine which encoder to select. Internal method.
### isLibsodiumSetupCorrectly()
> `public static` function isLibsodiumSetupCorrectly(`bool $echo = false`)
Returns `TRUE` if libsodium is set up correctly. `FALSE` otherwise.
Optionally, you may pass `true` to get verbose output on why it fails.
doc/Classes/Symmetric/AuthenticationKey.md 0000666 00000000373 13536431632 0014651 0 ustar 00 # AuthenticationKey extends [SecretKey](SecretKey.md)
**Namespace**: `\ParagonIE\Halite\Symmetric`
Represents an HMAC-SHA512/256 secret key.
* Asymmetric? no
* Public key? no
* Secret key? **YES**
* Encryption key? no
* Authentication key? **YES**
doc/Classes/Symmetric/SecretKey.md 0000666 00000000377 13536431632 0013123 0 ustar 00 # SecretKey extends [Key](../Key.md)
**Namespace**: `\ParagonIE\Halite\Symmetric`
Represents a secret key (Xsalsa20 or HMAC-SHA512/256)
* Asymmetric? no
* Public key? no
* Secret key? **YES**
* Encryption key? *unknown*
* Authentication key? *unknown*
doc/Classes/Symmetric/Crypto.md 0000666 00000005346 13536431632 0012506 0 ustar 00 # Crypto (abstract)
**Namespace**: `\ParagonIE\Halite\Symmetric`
## Methods
### `authenticate()`
> `public` authenticate(`string $message`, [`AuthenticationKey`](AuthenticationKey.md) `$secretKey`, `$encoding = Halite::ENCODE_BASE64URLSAFE`) : `string`
Calculate a MAC for a given message, using a secret authentication key.
### `encrypt()`
> `public` encrypt(`HiddenString $plaintext`, [`EncryptionKey`](EncryptionKey.md) `$secretKey`, `$encoding = Halite::ENCODE_BASE64URLSAFE`): `string`
Encrypt-then-authenticate a message. This method will:
1. Generate a random HKDF salt.
2. Split the [`EncryptionKey`](EncryptionKey.md) into an encryption key and
authentication key using salted HKDF.
3. Generate a random nonce.
4. Encrypt your plaintext (`$source`) with the derived encryption key (step 2).
5. MAC the ciphertext (step 4), along with the current library version, the HKDF
salt, and the nonce, with the derived authentication key (step 2).
6. Return the output of step 5 either as raw binary or as a hex-encoded string.
### `decrypt()`
> `public` decrypt(`string $ciphertext`, [`EncryptionKey`](EncryptionKey.md) `$secretKey`, `$encoding = Halite::ENCODE_BASE64URLSAFE`) : `HiddenString`
Verify-then-decrypt a message. This method will:
1. If we aren't expecting raw data, we treat `$source` as a hex string and
decode it to raw binary.
2. Parse the library version tag, HKDF salt, and nonce from the message.
3. Split the [`EncryptionKey`](EncryptionKey.md) into an encryption key and
authentication key using salted HKDF.
4. Verify the MAC using the derived authentication key (step 3).
5. If step 4 is successful, decrypt the ciphertext with the derived encryption
key (step 3).
6. Return what should be the original plaintext.
### `encryptWithAd()`
> `public` encryptWithAd(`HiddenString $plaintext`, [`EncryptionKey`](EncryptionKey.md) `$secretKey`, `string $additionalData = ''`, `$encoding = Halite::ENCODE_BASE64URLSAFE`): `string`
This is similar to `encrypt()`, except the `$additionalData` string is prepended to the ciphertext (after the nonce) when calculating the Message Authentication Code (MAC).
### `decryptWithAd()`
> `public` decryptWithAd(`string $ciphertext`, [`EncryptionKey`](EncryptionKey.md) `$secretKey`, `string $additionalData = ''`, `$encoding = Halite::ENCODE_BASE64URLSAFE`): `HiddenString`
This is similar to `decrypt()`, except the `$additionalData` string is prepended to the ciphertext (after the nonce) when calculating the Message Authentication Code (MAC).
### `verify()`
> `public` verify(`string $message`, [`AuthenticationKey`](AuthenticationKey.md) `$secretKey`, `string $mac`, `$encoding = Halite::ENCODE_BASE64URLSAFE`) : `boolean`
Verify the MAC for a given message and secret authentication key. doc/Classes/Symmetric/EncryptionKey.md 0000666 00000000360 13536431632 0014020 0 ustar 00 # EncryptionKey extends [SecretKey](SecretKey.md)
**Namespace**: `\ParagonIE\Halite\Symmetric`
Represents an Xsalsa20 secret key.
* Asymmetric? no
* Public key? no
* Secret key? **YES**
* Encryption key? **YES**
* Authentication key? no
doc/Classes/Symmetric/Config.md 0000666 00000000164 13536431632 0012424 0 ustar 00 # Config
**Namespace**: `\ParagonIE\Halite\Symmetric`
Encapsulates configuration in an immutable data structure.
doc/Classes/Config.md 0000666 00000000620 13536431632 0010445 0 ustar 00 # Config
**Namespace**: `\ParagonIE\Halite`
Encapsulates configuration in an immutable data structure.
## Methods
### `__get()`
> `public` __get(`string $key`)
Gets a configuration directive if it exists, or throws an [ConfigDirectiveNotFound](Alerts/ConfigDirectiveNotFound.md).
### `__set()`
> `public` __set()`
Returns false, does nothing. Configuration should not be altered at runtime.
doc/Classes/KeyPair.md 0000666 00000000312 13536431632 0010602 0 ustar 00 # KeyPair
**Namespace**: `\ParagonIE\Halite`
Base class for all KeyPair objects which should encapsulate a secret/public key
pair for asymmetric cryptography operations.
## Methods
### Constructor
doc/Classes/SignatureKeyPair.md 0000666 00000000603 13536431632 0012467 0 ustar 00 # SignatureKeyPair extends [KeyPair](KeyPair.md)
**Namespace**: `\ParagonIE\Halite`
This [`KeyPair`](KeyPair.md) class represents a secret key and public key used for generating digital signatures using
Ed25519.
* Secret Key: [`SignatureSecretKey`](Asymmetric/SignatureSecretKey.md)
* Public Key: [`SignaturePublicKey`](Asymmetric/SignaturePublicKey.md)
## Methods
### Constructor
doc/Install-Guides/Ubuntu.md 0000666 00000003124 13536431632 0011753 0 ustar 00 # Installing Halite on Ubuntu 16.04 and newer
Assuming that you don't have build utils, php7 devtools, git, pear&pecl, composer (fresh install)
## Libsodium
1. Get build utils
`sudo apt-get install build-essential`
2. Get php7.0-dev
`sudo apt-get install php7.0-dev`
*Or php7.1-dev/php7.2-dev*
`sudo apt-get install php7.1-dev`
`sudo apt-get install php7.2-dev`
3. Git (good)
`sudo apt-get install git`
4. Get libsodium
```
# Clone the libsodium source tree & Build libsodium, perform any defined tests, install libsodium
git clone -b stable https://github.com/jedisct1/libsodium.git && cd libsodium && ./configure && make check && make install
```
1. Get PEAR & PECL
`sudo apt-get install pear`
2. Install libsodium from PECL
`pecl install libsodium` (*or `pecl install -f libsodium-2.0.8` according to comments*)
3. Get straight to **/etc/php//mods-available/** and make a `libsodium.ini` file (*Where is 7.0 or 7.1 or 7.2*)
4. Write down `extension=libsodium.so` (*or `sodium.so` according to comments*) in `libsodium.ini` & save (**Yes, it works like this now**, no more php.ini bs)
5. Enable the libsodium mod
`sudo phpenmod libsodium`
6. Reload PHP
`sudo /etc/init.d/apache2 restart && service php7.0-fpm restart`
7. Check for libsodium with `php -m`
## Halite
1. Get composer
`sudo apt-get install composer`
2. Navigate to your php project folder and install halite
`composer require paragonie/halite`
3. Done
------
The above guide was contributed by [aolko](https://github.com/aolko) in [#48](https://github.com/paragonie/halite/issues/48).
doc/Features.md 0000666 00000013426 13536431632 0007431 0 ustar 00 # Halite Features
In addition to its [core functionality](Basic.md), Halite offers some useful
APIs for solving common problems.
* `Cookie` - Authenticated encryption for your HTTPS cookies
* `File` - Cryptography library for working with files
* `Password` - Secure password storage and password verification API
## Cookie Encryption/Decryption
Unlike the core Halite APIs, the Cookie class is not static. You must create an
instance of `Cookie` and work with it.
```php
$enc_key = \ParagonIE\Halite\KeyFactory::loadEncryptionKey('/path/to/key');
$cookie = new \ParagonIE\Halite\Cookie($enc_key);
```
From then on, all you need to do is use the `fetch()` and `store()` APIs.
**Storing** data in an encrypted cookie:
```php
$cookie->store(
'auth',
['s' => $selector, 'v' => $verifier],
time() + 2592000
);
```
**Fetching** data from an encrypted cookie:
```php
$token = $cookie->fetch('auth');
var_dump($token); // array(2) ...
```
## File Cryptography
Halite's `File` class provides streaming file cryptography features, such as
authenticated encryption and digital signatures. `File` allows developers to
perform secure cryptographic operations on large files with a low memory
footprint.
The `File` API looks like this:
* `File::checksum`(`file`, [`AuthenticationKey?`](Classes/Symmetric/AuthenticationKey.md), `bool?`): `string`
* `File::encrypt`(`file`, `file`, [`EncryptionKey`](Classes/Symmetric/EncryptionKey.md))
* `File::decrypt`(`file`, `file`, [`EncryptionKey`](Classes/Symmetric/EncryptionKey.md))
* `File::seal`(`file`, `file`, [`EncryptionPublicKey`](Classes/Asymmetric/EncryptionPublicKey.md))
* `File::unseal`(`file`, `file`, [`EncryptionSecretKey`](Classes/Asymmetric/EncryptionSecretKey.md))
* `File::sign`(`file`, [`EncryptionSecretKey`](Classes/Asymmetric/EncryptionSecretKey.md)): `string`
* `File::verify`(`file`, [`EncryptionPublicKey`](Classes/Asymmetric/EncryptionPublicKey.md)): `bool`
The `file` type indicates that the argument can be either a `string` containing
the file's path, or a `resource` (open file handle).
Each of these features is designed to work in a streaming fashion.
### Calculating the Checksum of a File
Basic usage:
```php
$checksum = \ParagonIE\Halite\File::checksum('/source/file/path');
```
If for some reason you desire to use a keyed hash rather than just a plain one,
you can pass an [`AuthenticationKey`](Classes/Symmetric/AuthenticationKey.md) to
an optional second parameter, or `null`.
```php
$keyed = \ParagonIE\Halite\File::checksum('/source/file/path', $auth_key);
```
Finally, you can pass `true` as the optional third argument if you would like a
raw binary string rather than a hexadecimal string.
```php
$keyed_checksum = \ParagonIE\Halite\File::checksum('/source/file/path', null, true);
```
### Symmetric-Key File Encryption / Decryption
If you need to encrypt a file larger than the amount of memory available to PHP,
you'll run into problems with just the basic `\ParagonIE\Halite\Symmetric\Crypto`
API. To work around these limitations, use `File::encrypt()` instead.
For example:
```php
\ParagonIE\Halite\File::encrypt(
$inputFilename,
$outputFilename,
$enc_key
);
```
This will encrypt the contents of the file located at `$inputFilename` and write
its contents to `$outputFilename`.
Decryption is straightforward as well:
```php
\ParagonIE\Halite\File::decrypt(
$inputFilename,
$outputFilename,
$enc_key
);
```
### Asymmetric-Key File Encryption / Decryption
This feature encrypts files with a public key so that they can be decrypted
offline with a secret key.
```php
$seal_keypair = \ParagonIE\Halite\KeyFactory::generateEncryptionKeyPair();
$seal_secret = $seal_keypair->getSecretKey();
$seal_public = $seal_keypair->getPublicKey();
```
**Sealing** the contents of a file using Halite:
```php
\ParagonIE\Halite\File::seal(
$inputFilename,
$outputFilename,
$seal_public
);
```
**Opening** the contents of a sealed file using Halite:
```php
\ParagonIE\Halite\File::unseal(
$inputFilename,
$outputFilename,
$seal_secret
);
```
### Asymmetric-Key Digital Signatures
First, you need a key pair.
```php
$sign_keypair = \ParagonIE\Halite\KeyFactory::generateSignatureKeyPair();
$sign_secret = $sign_keypair->getSecretKey();
$sign_public = $sign_keypair->getPublicKey();
```
**Signing** the contents of a file using your secret key:
```php
$signature = \ParagonIE\Halite\File::sign(
$inputFilename,
$sign_secret
);
```
**Verifying** the contents of a file using a known public key:
```php
$valid = \ParagonIE\Halite\File::verify(
$inputFilename,
$sign_public,
$signature
);
```
Like `checksumFile()`, you can pass an optional `true` to get a raw binary
signature instead of a hexadecimal-encoded string.
## Secure Password Storage
This feature serves a very narrow use case: You have the webserver and database
on separate hardware, and would like to prevent a database compromise from
leaking the actual password hashes.
If your webserver and database server are the same machine, there is no
advantage to using this feature over [libsodium's Argon2 implementation](https://paragonie.com/book/pecl-libsodium/read/07-password-hashing.md#crypto-pwhash-str).
**Hashing then Encrypting** a password:
```php
$stored_hash = \ParagonIE\Halite\Password::hash(
$plaintext_password, // HiddenString
$encryption_key // \ParagonIE\Halite\Symmetric\EncryptionKey
);
```
**Validating a password**:
```php
try {
if (\ParagonIE\Halite\Password::verify(
$plaintext_password, // HidddenString
$stored_hash, // string
$encryption_key // \ParagonIE\Halite\Symmetric\EncryptionKey
)) {
// Password matches
}
} catch (\ParagonIE\Halite\Alerts\InvalidMessage $ex) {
// Handle an invalid message here. This usually means tampered ciphertext.
}
```
doc/Basic.md 0000666 00000022266 13536431632 0006676 0 ustar 00 # Basic Halite Usage
This is the Basic Halite API:
* Encryption
* Symmetric
* `Symmetric\Crypto::encrypt`(`HiddenString`, [`EncryptionKey`](Classes/Symmetric/EncryptionKey.md), `bool?`): `string`
* `Symmetric\Crypto::decrypt`(`string`, [`EncryptionKey`](Classes/Symmetric/EncryptionKey.md), `bool?`): `HiddenString`
* Asymmetric
* Anonymous
* `Asymmetric\Crypto::seal`(`HiddenString`, [`EncryptionPublicKey`](Classes/Asymmetric/EncryptionPublicKey.md), `bool?`): `string`
* `Asymmetric\Crypto::unseal`(`string`, [`EncryptionSecretKey`](Classes/Asymmetric/EncryptionSecretKey.md), `bool?`): `HiddenString`
* Authenticated
* `Asymmetric\Crypto::encrypt`(`HiddenString`, [`EncryptionSecretKey`](Classes/Asymmetric/EncryptionSecretKey.md), [`EncryptionPublicKey`](Classes/Asymmetric/EncryptionPublicKey.md), `bool?`): `string`
* `Asymmetric\Crypto::decrypt`(`string`, [`EncryptionSecretKey`](Classes/Asymmetric/EncryptionSecretKey.md), [`EncryptionPublicKey`](Classes/Asymmetric/EncryptionPublicKey.md), `bool?`): `HiddenString`
* Authentication
* Symmetric
* `Symmetric\Crypto::authenticate`(`string`, [`AuthenticationKey`](Classes/Symmetric/AuthenticationKey.md), `bool?`): `string`
* `Symmetric\Crypto::verify`(`string`, [`AuthenticationKey`](Classes/Symmetric/AuthenticationKey.md), `string`, `bool?`): `bool`
* Asymmetric
* `Asymmetric\Crypto::sign`(`string`, [`SignatureSecretKey`](Classes/Asymmetric/SignatureSecretKey.md), `bool?`): `string`
* `Asymmetric\Crypto::verify`(`string`, [`SignaturePublicKey`](Classes/Asymmetric/SignaturePublicKey.md), `string`, `bool?`): `bool`
Most of the other [Halite features](Features.md) build on top of these simple APIs.
## Fundamentals
If you're not sure what any of the terms on this page mean, you might be better
served reading our [guide to cryptography terms and concepts for PHP developers](https://paragonie.com/blog/2015/08/you-wouldnt-base64-a-password-cryptography-decoded).
## Error Handling
Unless stated otherwise, any time Halite encounters invalid data (an attacker
tampered with the ciphertext, you have the wrong decryption key, etc.), Halite
will throw a typed [`Exception`](Classes/Alerts). If you catch one, you should
log the incident and fail closed (i.e. terminate the script gracefully) rather
than proceeding as if nothing happened.
For authentication functions, Halite will typically just return `false`.
## Encryption
Encryption functions expect your message to be encapsulated in an instance
of the [`HiddenString`](https://github.com/paragonie/hidden-string) class. Decryption functions
will return the decrypted plaintext in a `HiddenString` object.
### Symmetric-Key Encryption
First, you'll need is an encryption key. The easiest way to obtain one is to
generate it:
```php
use ParagonIE\Halite\KeyFactory;
$enc_key = KeyFactory::generateEncryptionKey();
```
This generates a strong random key. If you'd like to reuse it, you can simply
store it in a file.
```php
KeyFactory::save($enc_key, '/path/to/encryption.key');
```
Later, you can load it like so:
```php
$enc_key = KeyFactory::loadEncryptionKey('/path/to/encryption.key');
```
Or if you want to store it in a string
```php
$key_hex = KeyFactory::export($enc_key)->getString();
```
and get it back later
```php
$enc_key = KeyFactory::importEncryptionKey(new HiddenString($key_hex));
```
--------------------------------------------------------------------------------
**Encryption** should be rather straightforward:
```php
use ParagonIE\HiddenString\HiddenString;
$ciphertext = \ParagonIE\Halite\Symmetric\Crypto::encrypt(
new HiddenString(
"Your message here. Any string content will do just fine."
),
$enc_key
);
```
By default, `Crypto::encrypt()` will return a hexadecimal encoded string. If you
want raw binary, simply pass `true` as the third argument (similar to the API
used by PHP's `hash()` function).
The inverse operation, **decryption** is congruent:
```php
$plaintext = \ParagonIE\Halite\Symmetric\Crypto::decrypt(
$ciphertext,
$enc_key
);
```
The important thing to keep in mind is that `$enc_key` is not a string, it is an
instance of `\ParagonIE\Halite\Symmetric\EncryptionKey`.
If you're attempting to decrypt a raw binary string rather than a hex-encoded
string, pass `true` to the third argument of `Crypto::decrypt`.
### Authenticated Asymmetric-Key Encryption (Encrypting)
This API facilitates message encryption between to participants in a
conversation. It requires your secret key and your partner's public key.
Assuming you are Alice, you would generate your keypair like so. (The other
person, Bob, will do the same on his end.)
```php
$alice_keypair = \ParagonIE\Halite\KeyFactory::generateEncryptionKeyPair();
$alice_secret = $alice_keypair->getSecretKey();
$alice_public = $alice_keypair->getPublicKey();
$send_to_bob = sodium_bin2hex($alice_public->getRawKeyMaterial());
```
Alice will then load Bob's public key into the appropriate object like so:
```php
use ParagonIE\HiddenString\HiddenString;
$bob_public = new \ParagonIE\Halite\Asymmetric\EncryptionPublicKey(
new HiddenString(
sodium_hex2bin($recv_from_bob)
)
);
```
--------------------------------------------------------------------------------
**Encrypting** a message from Alice to send to Bob:
```php
$send_to_bob = \ParagonIE\Halite\Asymmetric\Crypto::encrypt(
new HiddenString(
"Your message here. Any string content will do just fine."
),
$alice_secret,
$bob_public
);
```
As with symmetric-key encryption, this defaults to hexadecimal encoded output.
If you desire raw binary, you can pass an optional `true` as the fourth argument
to `Crypto::encrypt()`.
**Decrypting** a message that Alice received from Bob:
```php
$message = \ParagonIE\Halite\Asymmetric\Crypto::decrypt(
$received_ciphertext,
$alice_secret,
$bob_public
);
```
### Anonymous Asymmetric-Key Encryption (Sealing)
A sealing interface is one where you encrypt a message with a public key, such
that only the person possessing the corresponding secret key can decrypt it.
If you wish to seal information, you only need one keypair rather than two:
```php
$seal_keypair = \ParagonIE\Halite\KeyFactory::generateEncryptionKeyPair();
$seal_secret = $seal_keypair->getSecretKey();
$seal_public = $seal_keypair->getPublicKey();
```
You want to only keep `$seal_public` stored outside of the trusted environment.
--------------------------------------------------------------------------------
**Encrypting** an anonymous message:
```php
use ParagonIE\HiddenString\HiddenString;
$sealed = \ParagonIE\Halite\Asymmetric\Crypto::seal(
new HiddenString(
"Your message here. Any string content will do just fine."
),
$seal_public
);
```
Once again, this defaults to hexadecimal encoded output. If you desire raw
binary, you can pass an optional `true` as the fourth argument to
`Crypto::seal()`.
**Decrypting** an anonymous message:
```php
$opened = \ParagonIE\Halite\Asymmetric\Crypto::unseal(
$sealed,
$seal_secret
);
```
## Authentication
### Symmetric-Key Authentication
Symmetric-key authentication is useful if you'd like to authenticate, but not
encrypt, some information that you transfer over a network or share with your
end users.
First, you will need an appropriate key. The easiest way to get one is to simply
generate one randomly then store it for reuse (similar to secret-key encryption
above):
```php
$auth_key = \ParagonIE\Halite\KeyFactory::generateAuthenticationKey();
```
--------------------------------------------------------------------------------
**Authenticating** a message:
```php
// MAC stands for Message Authentication Code
$mac = \ParagonIE\Halite\Symmetric\Crypto::authenticate(
"Your message here. Any string content will do just fine.",
$auth_key
);
```
**Verifying** a message, given the message and a message authentication code:
```php
$valid = \ParagonIE\Halite\Symmetric\Crypto::verify(
"Your message here. Any string content will do just fine.",
$auth_key,
$mac
);
if ($valid) {
// Success!
}
```
By default, `$mac` will be hex-encoded. If you need a raw binary string, pass
`true` as the third (optional) argument to `Crypto::authenticate()`. You will
also need to pass `true` as the fourth (optional) argument in `Crypto::verify()`.
### Asymmetric-Key Authentication (Digital Signatures)
As with anonymous asymmetric-key encryption, you only need one keypair and you
only give out your public key.
```php
$sign_keypair = \ParagonIE\Halite\KeyFactory::generateSignatureKeyPair();
$sign_secret = $sign_keypair->getSecretKey();
$sign_public = $sign_keypair->getPublicKey();
```
--------------------------------------------------------------------------------
**Signing** a message with a secret key:
```php
$signature = \ParagonIE\Halite\Asymmetric\Crypto::sign(
"Your message here. Any string content will do just fine.",
$sign_secret
);
```
**Verifying** the signature of a message with its corresponding public key:
```php
$valid = \ParagonIE\Halite\Asymmetric\Crypto::verify(
"Your message here. Any string content will do just fine.",
$sign_public,
$signature
);
```
The typical rules for hex-encoding apply here as well.
doc/Examples/01-passwords.php 0000666 00000001507 13536431632 0012060 0 ustar 00 getMessage(), PHP_EOL;
echo $ex->getTraceAsString(), PHP_EOL;
exit(127);
}
composer.json 0000666 00000002574 13536431632 0007310 0 ustar 00 {
"name": "paragonie/halite",
"type": "library",
"keywords": [
"encryption",
"public-key",
"cryptography",
"signatures",
"password",
"hashing",
"libsodium",
"sodium",
"Curve25519",
"X25519",
"Ed25519",
"XSalsa20",
"BLAKE",
"BLAKE2",
"BLAKE2b",
"Argon2",
"Argon2i",
"ext-sodium"
],
"description": "High-level cryptography interface powered by libsodium",
"homepage": "https://github.com/paragonie/halite",
"license": "MPL-2.0",
"authors": [
{
"name": "Paragon Initiative Enterprises",
"homepage": "https://paragonie.com",
"email": "info@paragonie.com"
}
],
"require": {
"php": "^7.2",
"paragonie/constant_time_encoding": "^2",
"paragonie/hidden-string": "^1",
"paragonie/sodium_compat": "^1.11"
},
"autoload": {
"psr-4": {
"ParagonIE\\Halite\\": "./src"
}
},
"require-dev": {
"phpunit/phpunit": "^8",
"vimeo/psalm": "^3"
},
"support": {
"docs": "https://github.com/paragonie/halite/tree/master/doc"
},
"config": {
"preferred-install": "dist",
"optimize-autoloader": true,
"sort-packages": true
},
"prefer-stable": true
}
.travis.yml 0000666 00000001373 13536431632 0006673 0 ustar 00 language: php
sudo: required
dist: trusty
php:
- "7.2"
- "7.3"
- "master"
- "nightly"
matrix:
fast_finish: true
allow_failures:
- php: "master"
- php: "nightly"
install:
- travis_retry composer install --no-interaction
- wget -c -nc --retry-connrefused --tries=0 https://github.com/php-coveralls/php-coveralls/releases/download/v2.0.0/php-coveralls.phar
- chmod +x php-coveralls.phar
- php php-coveralls.phar --version
script:
- ./vendor/bin/phpunit --coverage-clover build/logs/clover.xml
- ./vendor/bin/psalm
after_success:
- travis_retry php php-coveralls.phar -v
before_script:
- mkdir -p build/logs
- ls -al
cache:
directories:
- vendor
- $HOME/.cache/composer
phpunit.xml.dist 0000666 00000003175 13536431632 0007737 0 ustar 00
./src
./libsodium
./vendor
./build
./stub
./test
./doc
./test/unit
test/random_audit.php 0000666 00000003072 13536431632 0010716 0 ustar 00 $f) {
// Prevent . and .. from being treated as valid files:
$check = preg_replace('#^(.+?)/([^/]+)$#', '$2', $f);
if ($check === '.' || $check === '..') {
unset($file[$i]);
}
}
}
$fileList = array_merge($fileList, $file);
}
return $fileList;
}
if ($argc > 1) {
$extensions = array_slice($argv, 1);
} else {
$extensions = ['php', 'twig'];
}
$fileList = [];
foreach ($extensions as $ex) {
foreach (list_all_files(dirname(__DIR__) . '/src/', $ex) as $file) {
$fileList []= $file;
}
}
$choice = random_int(0, count($fileList) - 1);
echo "Audit this file:\n\t";
$l = strlen(dirname(__DIR__));
echo substr($fileList[$choice], $l), "\n";
test/unit/HiddenStringTest.php 0000666 00000000651 13536431632 0012451 0 ustar 00 assertInstanceOf(Outsourced::class, $x);
}
} test/unit/SymmetricTest.php 0000666 00000021717 13536431632 0012051 0 ustar 00 assertTrue(
Symmetric::verify($message, $key, $mac)
);
}
/**
* @throws CryptoException\InvalidKey
* @throws CryptoException\InvalidMessage
* @throws CryptoException\InvalidSignature
* @throws CryptoException\InvalidType
* @throws Exception
* @throws TypeError
*/
public function testAuthenticateFail()
{
$key = new AuthenticationKey(new HiddenString(str_repeat('A', 32), true));
$message = 'test message';
$mac = Symmetric::authenticate($message, $key, true);
// Test invalid message
$this->assertFalse(
Symmetric::verify('othermessage', $key, $mac, true)
);
$r = random_int(0, mb_strlen($mac, '8bit') - 1);
$_mac = $mac;
$_mac[$r] = chr(
ord($_mac[$r])
^
1 << random_int(0, 7)
);
// Test invalid signature
$this->assertFalse(
Symmetric::verify(
$message,
$key,
$_mac,
true
)
);
}
/**
* @throws CryptoException\CannotPerformOperation
* @throws CryptoException\InvalidDigestLength
* @throws CryptoException\InvalidKey
* @throws CryptoException\InvalidMessage
* @throws CryptoException\InvalidSignature
* @throws CryptoException\InvalidType
* @throws TypeError
*/
public function testEncrypt()
{
$key = new EncryptionKey(new HiddenString(str_repeat('A', 32)));
$message = Symmetric::encrypt(
new HiddenString('test message'),
$key
);
$this->assertSame(
strpos($message, Halite::VERSION_PREFIX),
0
);
$plain = Symmetric::decrypt($message, $key);
$this->assertSame($plain->getString(), 'test message');
}
/**
* @throws CryptoException\CannotPerformOperation
* @throws CryptoException\InvalidDigestLength
* @throws CryptoException\InvalidKey
* @throws CryptoException\InvalidMessage
* @throws CryptoException\InvalidSignature
* @throws CryptoException\InvalidType
* @throws TypeError
*/
public function testEncryptLarge()
{
$msg = str_repeat("\xff", 1 << 17);
$key = new EncryptionKey(new HiddenString(str_repeat('A', 32)));
$message = Symmetric::encrypt(
new HiddenString($msg),
$key
);
$this->assertSame(
strpos($message, Halite::VERSION_PREFIX),
0
);
$plain = Symmetric::decrypt($message, $key);
$this->assertSame($plain->getString(), $msg);
}
/**
* @throws CryptoException\CannotPerformOperation
* @throws CryptoException\InvalidDigestLength
* @throws CryptoException\InvalidKey
* @throws CryptoException\InvalidMessage
* @throws CryptoException\InvalidSignature
* @throws CryptoException\InvalidType
* @throws TypeError
*/
public function testEncryptWithAd()
{
$key = new EncryptionKey(new HiddenString(str_repeat('A', 32)));
$message = Symmetric::encryptWithAd(
new HiddenString('test message'),
$key,
'test'
);
$this->assertSame(
strpos($message, Halite::VERSION_PREFIX),
0
);
$plain = Symmetric::decryptWithAd($message, $key, 'test');
$this->assertSame($plain->getString(), 'test message');
try {
Symmetric::decryptWithAd($message, $key, 'wrong');
$this->fail('AD did not change MAC');
} catch (CryptoException\InvalidMessage $ex) {
$this->assertSame(
'Invalid message authentication code',
$ex->getMessage()
);
}
}
/**
* @throws CryptoException\CannotPerformOperation
* @throws CryptoException\InvalidDigestLength
* @throws CryptoException\InvalidKey
* @throws CryptoException\InvalidMessage
* @throws CryptoException\InvalidSignature
* @throws CryptoException\InvalidType
* @throws TypeError
*/
public function testEncryptEmpty()
{
$key = new EncryptionKey(new HiddenString(str_repeat('A', 32)));
$message = Symmetric::encrypt(new HiddenString(''), $key);
$this->assertSame(
strpos($message, Halite::VERSION_PREFIX),
0
);
$plain = Symmetric::decrypt($message, $key);
$this->assertSame($plain->getString(), '');
}
/**
* @throws CryptoException\CannotPerformOperation
* @throws CryptoException\InvalidDigestLength
* @throws CryptoException\InvalidKey
* @throws CryptoException\InvalidMessage
* @throws CryptoException\InvalidSignature
* @throws CryptoException\InvalidType
* @throws TypeError
*/
public function testRawEncrypt()
{
$key = new EncryptionKey(new HiddenString(str_repeat('A', 32)));
$message = Symmetric::encrypt(new HiddenString('test message'), $key, true);
$this->assertTrue(strpos($message, Halite::HALITE_VERSION) === 0);
$plain = Symmetric::decrypt($message, $key, true);
$this->assertSame($plain->getString(), 'test message');
}
/**
* @throws CryptoException\CannotPerformOperation
* @throws CryptoException\InvalidDigestLength
* @throws CryptoException\InvalidKey
* @throws CryptoException\InvalidMessage
* @throws CryptoException\InvalidSignature
* @throws CryptoException\InvalidType
* @throws Exception
* @throws TypeError
*/
public function testEncryptFail()
{
$key = new EncryptionKey(new HiddenString(str_repeat('A', 32)));
$message = Symmetric::encrypt(
new HiddenString('test message'),
$key,
true
);
$r = random_int(0, mb_strlen($message, '8bit') - 1);
$message[$r] = chr(
ord($message[$r])
^
1 << random_int(0, 7)
);
try {
$plain = Symmetric::decrypt($message, $key, true);
$this->assertSame($plain, $message);
$this->fail(
'This should have thrown an InvalidMessage exception!'
);
} catch (CryptoException\InvalidMessage $e) {
$this->assertTrue($e instanceof CryptoException\InvalidMessage);
}
}
/**
* @throws CryptoException\CannotPerformOperation
* @throws CryptoException\InvalidDigestLength
* @throws CryptoException\InvalidKey
* @throws CryptoException\InvalidMessage
* @throws CryptoException\InvalidType
* @throws Exception
* @throws TypeError
*/
public function testUnpack()
{
$key = new EncryptionKey(new HiddenString(str_repeat('A', 32)));
// Randomly sized plaintext
$size = random_int(1, 1024);
$plaintext = random_bytes($size);
$message = Symmetric::encrypt(
new HiddenString($plaintext),
$key,
true
);
// Let's unpack our message
$unpacked = Symmetric::unpackMessageForDecryption($message);
// Now to test our expected results!
$this->assertSame(Binary::safeStrlen($unpacked[0]), Halite::VERSION_TAG_LEN);
$this->assertTrue($unpacked[1] instanceof Config);
$config = $unpacked[1];
if ($config instanceof Config) {
$this->assertSame(Binary::safeStrlen($unpacked[2]), $config->HKDF_SALT_LEN);
$this->assertSame(Binary::safeStrlen($unpacked[3]), SODIUM_CRYPTO_STREAM_NONCEBYTES);
$this->assertSame(
Binary::safeStrlen($unpacked[4]),
Binary::safeStrlen($message) - (
Halite::VERSION_TAG_LEN +
$config->HKDF_SALT_LEN +
SODIUM_CRYPTO_STREAM_NONCEBYTES +
$config->MAC_SIZE
)
);
$this->assertSame(Binary::safeStrlen($unpacked[5]), $config->MAC_SIZE);
} else {
$this->fail('Cannot continue');
}
}
}
test/unit/ConfigTest.php 0000666 00000004003 13536431632 0011267 0 ustar 00 12345
]);
$this->assertSame(12345, $config->abc);
try {
$x = $config->missing;
$this->fail('Missing configuration allowed');
} catch (ConfigDirectiveNotFound $ex) {
$this->assertSame('missing', $ex->getMessage());
}
}
public function testSymmetricConfig()
{
try {
$config = SymmetricConfig::getConfig('');
$this->fail('Invalid header allowed');
} catch (\ParagonIE\Halite\Alerts\InvalidMessage $ex) {
$this->assertSame('Invalid version tag', $ex->getMessage());
}
try {
$config = SymmetricConfig::getConfig('abcd');
$this->fail('Invalid header allowed');
} catch (\ParagonIE\Halite\Alerts\InvalidMessage $ex) {
$this->assertSame('Invalid version tag', $ex->getMessage());
}
try {
$config = SymmetricConfig::getConfig("\x31\x42\x00\x00", 'seal');
$this->fail('Invalid mode allowed');
} catch (\ParagonIE\Halite\Alerts\InvalidMessage $ex) {
$this->assertSame('Invalid configuration mode: seal', $ex->getMessage());
}
try {
$config = SymmetricConfig::getConfigEncrypt(1, 0);
$this->fail('Unsupported mode allowed');
} catch (\ParagonIE\Halite\Alerts\InvalidMessage $ex) {
$this->assertSame('Invalid version tag', $ex->getMessage());
}
try {
$config = SymmetricConfig::getConfigAuth(1, 0);
$this->fail('Unsupported mode allowed');
} catch (\ParagonIE\Halite\Alerts\InvalidMessage $ex) {
$this->assertSame('Invalid version tag', $ex->getMessage());
}
}
}
test/unit/AsymmetricTest.php 0000666 00000035441 13536431632 0012211 0 ustar 00 getSecretKey(),
$bob->getPublicKey()
);
$this->assertSame(
strpos($message, Halite::VERSION_PREFIX),
0
);
$plain = Asymmetric::decrypt(
$message,
$bob->getSecretKey(),
$alice->getPublicKey()
);
$this->assertSame($plain->getString(), 'test message');
}
/**
* @throws CryptoException\CannotPerformOperation
* @throws CryptoException\InvalidDigestLength
* @throws CryptoException\InvalidKey
* @throws CryptoException\InvalidMessage
* @throws CryptoException\InvalidSignature
* @throws CryptoException\InvalidType
* @throws Exception
* @throws TypeError
*/
public function testEncryptWithAd()
{
$alice = KeyFactory::generateEncryptionKeyPair();
$bob = KeyFactory::generateEncryptionKeyPair();
$random = random_bytes(32);
$message = Asymmetric::encryptWithAd(
new HiddenString('test message'),
$alice->getSecretKey(),
$bob->getPublicKey(),
$random
);
$this->assertSame(
strpos($message, Halite::VERSION_PREFIX),
0
);
$plain = Asymmetric::decryptWithAd(
$message,
$bob->getSecretKey(),
$alice->getPublicKey(),
$random
);
$this->assertSame($plain->getString(), 'test message');
try {
Asymmetric::decrypt(
$message,
$bob->getSecretKey(),
$alice->getPublicKey()
);
$this->fail('AD did not change MAC');
} catch (CryptoException\InvalidMessage $ex) {
$this->assertSame(
'Invalid message authentication code',
$ex->getMessage()
);
}
try {
Asymmetric::decryptWithAd(
$message,
$bob->getSecretKey(),
$alice->getPublicKey(),
'wrong'
);
$this->fail('AD did not change MAC');
} catch (CryptoException\InvalidMessage $ex) {
$this->assertSame(
'Invalid message authentication code',
$ex->getMessage()
);
}
}
/**
* @throws CryptoException\CannotPerformOperation
* @throws CryptoException\InvalidDigestLength
* @throws CryptoException\InvalidKey
* @throws CryptoException\InvalidMessage
* @throws CryptoException\InvalidSignature
* @throws CryptoException\InvalidType
* @throws TypeError
*/
public function testEncryptEmpty()
{
$alice = KeyFactory::generateEncryptionKeyPair();
$bob = KeyFactory::generateEncryptionKeyPair();
$message = Asymmetric::encrypt(
new HiddenString(''),
$alice->getSecretKey(),
$bob->getPublicKey()
);
$this->assertSame(
strpos($message, Halite::VERSION_PREFIX),
0
);
$plain = Asymmetric::decrypt(
$message,
$alice->getSecretKey(),
$bob->getPublicKey()
);
$this->assertSame('', $plain->getString());
}
public function testEncryptFail()
{
$alice = KeyFactory::generateEncryptionKeyPair();
$bob = KeyFactory::generateEncryptionKeyPair();
$message = Asymmetric::encrypt(
new HiddenString('test message'),
$alice->getSecretKey(),
$bob->getPublicKey(),
true
);
$r = random_int(0, mb_strlen($message, '8bit') - 1);
$amt = random_int(0, 7);
$message[$r] = chr(ord($message[$r]) ^ 1 << $amt);
try {
$plain = Asymmetric::decrypt(
$message,
$bob->getSecretKey(),
$alice->getPublicKey(),
true
);
$this->assertSame($plain, $message);
$this->fail(
'This should have thrown an InvalidMessage exception!'
);
} catch (CryptoException\InvalidMessage $e) {
$this->assertTrue($e instanceof CryptoException\InvalidMessage);
}
}
/**
* @throws CryptoException\InvalidKey
* @throws CryptoException\InvalidMessage
* @throws CryptoException\InvalidType
* @throws TypeError
*/
public function testSeal()
{
if (
SODIUM_LIBRARY_MAJOR_VERSION < 7 ||
(SODIUM_LIBRARY_MAJOR_VERSION == 7 && SODIUM_LIBRARY_MINOR_VERSION < 5)
) {
$this->markTestSkipped("Your version of libsodium is too old");
}
$alice = KeyFactory::generateEncryptionKeyPair();
$enc_secret = $alice->getSecretKey();
$enc_public = $alice->getPublicKey();
$this->assertSame(
sodium_crypto_box_publickey_from_secretkey($enc_secret->getRawKeyMaterial()),
$enc_public->getRawKeyMaterial()
);
$message = new HiddenString('This is for your eyes only');
$kp = sodium_crypto_box_keypair();
$test = sodium_crypto_box_seal($message->getString(), sodium_crypto_box_publickey($kp));
$decr = sodium_crypto_box_seal_open($test, $kp);
$this->assertTrue($decr !== false);
$sealed = Asymmetric::seal(
$message,
new EncryptionPublicKey(
new HiddenString(sodium_crypto_box_publickey($kp))
)
);
$opened = Asymmetric::unseal(
$sealed,
new EncryptionSecretKey(
new HiddenString(sodium_crypto_box_secretkey($kp))
)
);
$this->assertSame($opened->getString(), $message->getString());
$sealed = Asymmetric::seal($message, $enc_public);
$opened = Asymmetric::unseal($sealed, $enc_secret);
$this->assertSame($opened->getString(), $message->getString());
$sealed_raw = Asymmetric::seal($message, $alice->getPublicKey());
$opened_raw = Asymmetric::unseal($sealed_raw, $alice->getSecretKey());
$this->assertSame($opened_raw->getString(), $message->getString());
}
/**
* @throws CryptoException\InvalidKey
* @throws CryptoException\InvalidType
* @throws Exception
* @throws TypeError
*/
public function testSealFail()
{
if (
SODIUM_LIBRARY_MAJOR_VERSION < 7 ||
(SODIUM_LIBRARY_MAJOR_VERSION == 7 && SODIUM_LIBRARY_MINOR_VERSION < 5)
) {
$this->markTestSkipped("Your version of libsodium is too old");
}
$alice = KeyFactory::generateEncryptionKeyPair();
$message = new HiddenString('This is for your eyes only');
$sealed = Asymmetric::seal($message, $alice->getPublicKey(), true);
// Let's flip one bit, randomly:
$r = random_int(0, mb_strlen($sealed, '8bit') - 1);
$amt = 1 << random_int(0, 7);
$sealed[$r] = chr(ord($sealed[$r]) ^ $amt);
// This should throw an exception
try {
$opened = Asymmetric::unseal($sealed, $alice->getSecretKey());
$this->assertSame($opened->getString(), $message);
$this->fail(
'This should have thrown an InvalidMessage exception!'
);
} catch (CryptoException\InvalidKey $e) {
$this->assertTrue($e instanceof CryptoException\InvalidKey);
} catch (CryptoException\InvalidMessage $e) {
$this->assertTrue($e instanceof CryptoException\InvalidMessage);
}
}
/**
* @throws CryptoException\InvalidKey
* @throws CryptoException\InvalidSignature
* @throws CryptoException\InvalidType
* @throws TypeError
*/
public function testSign()
{
$alice = KeyFactory::generateSignatureKeyPair();
$message = 'test message';
$signature = Asymmetric::sign($message, $alice->getSecretKey());
$this->assertTrue(strlen($signature) === 88);
$this->assertTrue(
Asymmetric::verify($message, $alice->getPublicKey(), $signature)
);
}
/**
* @throws CryptoException\CannotPerformOperation
* @throws CryptoException\InvalidDigestLength
* @throws CryptoException\InvalidKey
* @throws CryptoException\InvalidMessage
* @throws CryptoException\InvalidSignature
* @throws CryptoException\InvalidType
* @throws TypeError
*/
public function testSignEncrypt()
{
$alice = KeyFactory::generateSignatureKeyPair();
$bob = KeyFactory::generateEncryptionKeyPair();
// http://time.com/4261796/tim-cook-transcript/
$message = new HiddenString(
'When I think of civil liberties I think of the founding principles of the country. ' .
'The freedoms that are in the First Amendment. But also the fundamental right to privacy.'
);
$encrypted = Asymmetric::signAndEncrypt($message, $alice->getSecretKey(), $bob->getPublicKey());
$decrypted = Asymmetric::verifyAndDecrypt($encrypted, $alice->getPublicKey(), $bob->getSecretKey());
$this->assertSame(
$message->getString(),
$decrypted->getString()
);
// Now with a signature key pair:
$bob = KeyFactory::generateSignatureKeyPair();
$encrypted = Asymmetric::signAndEncrypt($message, $alice->getSecretKey(), $bob->getPublicKey());
$decrypted = Asymmetric::verifyAndDecrypt($encrypted, $alice->getPublicKey(), $bob->getSecretKey());
$this->assertSame(
$message->getString(),
$decrypted->getString()
);
}
/**
* @throws CryptoException\CannotPerformOperation
* @throws CryptoException\InvalidDigestLength
* @throws CryptoException\InvalidKey
* @throws CryptoException\InvalidMessage
* @throws CryptoException\InvalidSignature
* @throws CryptoException\InvalidType
* @throws Exception
* @throws TypeError
*/
public function testSignEncryptFail()
{
$alice = KeyFactory::generateSignatureKeyPair();
$bob = KeyFactory::generateEncryptionKeyPair();
// http://time.com/4261796/tim-cook-transcript/
$junk = new HiddenString(
// Instead of a signature, it's 64 random bytes
random_bytes(SODIUM_CRYPTO_SIGN_BYTES) .
'When I think of civil liberties I think of the founding principles of the country. ' .
'The freedoms that are in the First Amendment. But also the fundamental right to privacy.'
);
$sealed = Asymmetric::encrypt(
$junk,
$alice->getSecretKey()->getEncryptionSecretKey(),
$bob->getPublicKey()
);
try {
Asymmetric::verifyAndDecrypt(
$sealed,
$alice->getPublicKey(),
$bob->getSecretKey()
);
$this->fail('Invalid signature was accepted.');
} catch (CryptoException\InvalidSignature $ex) {
$this->assertTrue(true);
}
// http://time.com/4261796/tim-cook-transcript/
$message = new HiddenString(
'When I think of civil liberties I think of the founding principles of the country. ' .
'The freedoms that are in the First Amendment. But also the fundamental right to privacy.'
);
try {
Asymmetric::signAndEncrypt(
$message,
$alice->getSecretKey(),
new \ParagonIE\Halite\Asymmetric\PublicKey(
new HiddenString(
\random_bytes(32)
)
)
);
$this->fail('Invalid public key was accepted');
} catch (CryptoException\InvalidKey $ex) {
}
$encrypted = Asymmetric::signAndEncrypt($message, $alice->getSecretKey(), $bob->getPublicKey());
try {
Asymmetric::verifyAndDecrypt(
$encrypted,
$alice->getPublicKey(),
new \ParagonIE\Halite\Asymmetric\SecretKey(
new HiddenString(
\random_bytes(32)
)
)
);
$this->fail('Invalid secret key was accepted');
} catch (CryptoException\InvalidKey $ex) {
}
}
/**
* @throws CryptoException\InvalidKey
* @throws CryptoException\InvalidSignature
* @throws CryptoException\InvalidType
* @throws Exception
* @throws TypeError
*/
public function testSignFail()
{
$alice = KeyFactory::generateSignatureKeyPair();
$message = 'test message';
$signature = Asymmetric::sign($message, $alice->getSecretKey(), true);
$this->assertFalse(
Asymmetric::verify(
'wrongmessage',
$alice->getPublicKey(),
$signature,
true
)
);
$_signature = $signature;
// Let's flip one bit, randomly:
$r = random_int(0, mb_strlen($_signature, '8bit') - 1);
$_signature[$r] = chr(
ord($_signature[$r])
^
1 << random_int(0, 7)
);
$this->assertFalse(
Asymmetric::verify(
$message,
$alice->getPublicKey(),
$_signature,
true
)
);
for ($i = 0; $i < SODIUM_CRYPTO_SIGN_BYTES; ++$i) {
try {
Asymmetric::verify(
$message,
$alice->getPublicKey(),
\ParagonIE\ConstantTime\Binary::safeSubstr($_signature, 0, $i),
true
);
$this->fail('Exception was not triggered');
} catch (CryptoException\InvalidSignature $ex) {
}
}
}
}
test/unit/Structure/MerkleTreeTest.php 0000666 00000007721 13536431632 0014133 0 ustar 00 assertSame(1, MerkleTree::getSizeRoundedUp(1));
$this->assertSame(2, MerkleTree::getSizeRoundedUp(2));
$this->assertSame(4, MerkleTree::getSizeRoundedUp(3));
$this->assertSame(4, MerkleTree::getSizeRoundedUp(4));
$this->assertSame(8, MerkleTree::getSizeRoundedUp(5));
$this->assertSame(8, MerkleTree::getSizeRoundedUp(6));
$this->assertSame(8, MerkleTree::getSizeRoundedUp(7));
$this->assertSame(8, MerkleTree::getSizeRoundedUp(8));
}
public function testExpectedBehavior()
{
$treeA = new MerkleTree(
new Node('a'),
new Node('b'),
new Node('c'),
new Node('d'),
new Node('e')
);
$this->assertSame(
'6781891a87aa476454b74dc635c5cdebfc8f887438829ce2e81423f54906c058',
$treeA->getRoot()
);
$this->assertSame(
hex2bin('6781891a87aa476454b74dc635c5cdebfc8f887438829ce2e81423f54906c058'),
$treeA->getRoot(true)
);
$treeB = new MerkleTree(
new Node('a'),
new Node('b'),
new Node('c'),
new Node('d'),
new Node('e'),
new Node('e'),
new Node('e'),
new Node('e')
);
$this->assertSame(
$treeA->getRoot(),
$treeB->getRoot()
);
$this->assertSame(
$treeA->getRoot(true),
$treeB->getRoot(true)
);
$treeC = $treeA->getExpandedTree(
new Node('e'),
new Node('e'),
new Node('e')
);
$this->assertSame(
$treeA->getRoot(),
$treeC->getRoot()
);
$treeD = $treeA->getExpandedTree(
new Node('f'),
new Node('e'),
new Node('e')
);
$this->assertNotEquals(
$treeA->getRoot(),
$treeD->getRoot()
);
$emptyTree = new MerkleTree();
$this->assertSame(
'',
$emptyTree->getRoot()
);
}
public function testDifferentHashSize()
{
$treeA = new MerkleTree(
new Node('a'),
new Node('b'),
new Node('c'),
new Node('d'),
new Node('e')
);
$this->assertSame(
'6781891a87aa476454b74dc635c5cdebfc8f887438829ce2e81423f54906c058',
$treeA->getRoot()
);
$treeA->setHashSize(SODIUM_CRYPTO_GENERICHASH_BYTES_MAX);
$this->assertSame(
'0e97a7c708bc8350809ecbeb941d9338af894c37d5fbfb6c3aa2f7ee0bc798f07d7505f33c5b6a6200c191efc51d9c4c0fd2d1397fe7291628aee424ff9093c3',
$treeA->getRoot()
);
try {
$treeA->setHashSize(SODIUM_CRYPTO_GENERICHASH_BYTES_MIN - 1);
$this->fail('Invalid hash size accepted');
} catch (\ParagonIE\Halite\Alerts\InvalidDigestLength $ex) {
}
try {
$treeA->setHashSize(SODIUM_CRYPTO_GENERICHASH_BYTES_MAX + 1);
$this->fail('Invalid hash size accepted');
} catch (\ParagonIE\Halite\Alerts\InvalidDigestLength $ex) {
}
}
public function testPersonalizedHash()
{
$treeA = new MerkleTree(
new Node('a'),
new Node('b'),
new Node('c'),
new Node('d'),
new Node('e')
);
$this->assertSame(
'6781891a87aa476454b74dc635c5cdebfc8f887438829ce2e81423f54906c058',
$treeA->getRoot()
);
$treeA->setPersonalizationString('Halite unit test framework');
$this->assertSame(
'e912ee25c680b0e3ee30b52eec0f0d79b502e15c9091c19cec7afc3115260b78',
$treeA->getRoot()
);
}
}
test/unit/Structure/TrimmedMerkleTreeTest.php 0000666 00000007070 13536431632 0015452 0 ustar 00 assertSame(
'8dc7ee23d6b29df641ac78a8c56bb2e0379015eeb06e1a01feb8bb617d6272f6',
$treeA->getRoot()
);
$treeB = new TrimmedMerkleTree(
new Node('a'),
new Node('b'),
new Node('c'),
new Node('d'),
new Node('e'),
new Node('e'),
new Node('e'),
new Node('e')
);
$this->assertNotSame(
$treeA->getRoot(),
$treeB->getRoot()
);
$treeC = $treeA->getExpandedTree(
new Node('e'),
new Node('e'),
new Node('e')
);
$this->assertSame(
get_class($treeB),
get_class($treeC)
);
$this->assertSame(
$treeB->getRoot(),
$treeC->getRoot()
);
$treeD = $treeA->getExpandedTree(
new Node('f'),
new Node('e'),
new Node('e')
);
$this->assertNotEquals(
$treeA->getRoot(),
$treeD->getRoot()
);
$emptyTree = new TrimmedMerkleTree();
$this->assertSame(
'',
$emptyTree->getRoot()
);
}
public function testDifferentHashSize()
{
$treeA = new TrimmedMerkleTree(
new Node('a'),
new Node('b'),
new Node('c'),
new Node('d'),
new Node('e')
);
$this->assertSame(
'8dc7ee23d6b29df641ac78a8c56bb2e0379015eeb06e1a01feb8bb617d6272f6',
$treeA->getRoot()
);
$treeA->setHashSize(SODIUM_CRYPTO_GENERICHASH_BYTES_MAX);
$this->assertSame(
'd25cb4ca1eb8bbf882376a3d66a33c91c7005386b8312979a003110323495e9c6e91701837d59dc798b84eed8cfa59a4763b61c54bbe2c502b9386da88c938e1',
$treeA->getRoot()
);
}
public function testPersonalizedHash()
{
$treeA = new TrimmedMerkleTree(
new Node('a'),
new Node('b'),
new Node('c'),
new Node('d'),
new Node('e')
);
$this->assertSame(
'8dc7ee23d6b29df641ac78a8c56bb2e0379015eeb06e1a01feb8bb617d6272f6',
$treeA->getRoot()
);
$treeA->setPersonalizationString('Halite unit test framework');
$this->assertSame(
'ae2e4caf8f7da8ed84fb22157870e31fee0646f4757dcf1c01367817500a205d',
$treeA->getRoot()
);
}
public function testCompat()
{
$treeA = new MerkleTree(
new Node('a'),
new Node('b'),
new Node('c'),
new Node('d')
);
$treeB = new TrimmedMerkleTree(
new Node('a'),
new Node('b'),
new Node('c'),
new Node('d')
);
$this->assertSame(
$treeA->getRoot(),
$treeB->getRoot()
);
$personal = random_bytes(32);
$treeA->setPersonalizationString($personal);
$treeB->setPersonalizationString($personal);
$this->assertSame(
$treeA->getRoot(),
$treeB->getRoot()
);
}
}
test/unit/Structure/NodeTest.php 0000666 00000001516 13536431632 0012755 0 ustar 00 assertSame($stringData, $node->getData());
$this->assertSame(bin2hex($hash), $node->getHash());
$this->assertSame($hash, $node->getHash(true));
$extra = random_bytes(32);
$hash = sodium_crypto_generichash($stringData . $extra);
$this->assertSame(bin2hex($hash), $node->getExpandedNode($extra)->getHash());
$this->assertSame($hash, $node->getExpandedNode($extra)->getHash(true));
}
}
test/unit/HaliteTest.php 0000666 00000004100 13536431632 0011266 0 ustar 00 assertTrue(
Halite::isLibsodiumSetupCorrectly()
);
}
/**
* @throws CryptoException\InvalidType
* @throws Exception
* @throws TypeError
*/
public function testEncoding()
{
$random_bytes = random_bytes(31);
// Backwards compatibility:
$encoder = Halite::chooseEncoder(false);
$this->assertSame(
Hex::encode($random_bytes),
$encoder($random_bytes)
);
$encoder = Halite::chooseEncoder(true);
$this->assertSame(
null,
$encoder
);
// New encoding in version 3:
$encoder = Halite::chooseEncoder(Halite::ENCODE_HEX);
$this->assertSame(
Hex::encode($random_bytes),
$encoder($random_bytes)
);
$encoder = Halite::chooseEncoder(Halite::ENCODE_BASE32);
$this->assertSame(
Base32::encode($random_bytes),
$encoder($random_bytes)
);
$encoder = Halite::chooseEncoder(Halite::ENCODE_BASE32HEX);
$this->assertSame(
Base32Hex::encode($random_bytes),
$encoder($random_bytes)
);
$encoder = Halite::chooseEncoder(Halite::ENCODE_BASE64);
$this->assertSame(
Base64::encode($random_bytes),
$encoder($random_bytes)
);
$encoder = Halite::chooseEncoder(Halite::ENCODE_BASE64URLSAFE);
$this->assertSame(
Base64UrlSafe::encode($random_bytes),
$encoder($random_bytes)
);
try {
Halite::chooseEncoder('dsfargeg');
$this->fail('Invalid type allowed for Encoder');
} catch (\ParagonIE\Halite\Alerts\InvalidType $ex) {
}
}
}
test/unit/UtilTest.php 0000666 00000013630 13536431632 0011005 0 ustar 00
* @license http://opensource.org/licenses/GPL-3.0 GPL 3
* @link https://paragonie.com/project/halite
*/
final class UtilTest extends TestCase
{
/**
* @throws Exception
*/
public function testChrToInt()
{
$this->assertSame(0x61, Util::chrToInt("a"));
$this->assertSame(0xe0, Util::chrToInt("\xe0"));
try {
Util::chrToInt("ab");
} catch (\RangeException $ex) {
}
$random = random_int(0, 255);
$this->assertSame(
$random,
Util::chrToInt(Util::intToChr($random))
);
}
public function testIntArrayToString()
{
$this->assertSame("\x00\x01\x03\x04", Util::intArrayToString([0,1,3,4]), 'Unexpected result');
$this->assertSame("\x00\x01\x03\x04", Util::intArrayToString([256,257,259,260]), 'Masking failed');
}
/**
* @throws TypeError
*/
public function testStringToIntArray()
{
$this->assertSame([0,1,3,4], Util::stringToIntArray("\x00\x01\x03\x04"), 'Unexpected result');
}
/**
* BLAKE2b hash
*
* @throws CannotPerformOperation
* @throws TypeError
*/
public function testHash()
{
$this->assertSame(
Util::raw_hash(''),
"\x0e\x57\x51\xc0\x26\xe5\x43\xb2\xe8\xab\x2e\xb0\x60\x99\xda\xa1".
"\xd1\xe5\xdf\x47\x77\x8f\x77\x87\xfa\xab\x45\xcd\xf1\x2f\xe3\xa8"
);
$this->assertSame(
Util::hash('Large Hashron Collider'),
'6c9a1f2b06d1f13ae845873ad470ea5eb78866c60b3f1f46733e89aee898fa46'
);
}
/**
* BLAKE2b hash
*
* @throws CannotPerformOperation
* @throws TypeError
*/
public function testKeyedHash()
{
$key = Util::raw_hash('');
$this->assertSame(
Util::raw_keyed_hash('', $key),
"\x0a\x28\xe9\x66\xfb\x7a\x7d\x39\xfd\x0a\x4d\x12\xd6\xfb\x14\x62".
"\x5b\x94\xb1\x73\x89\x43\x33\x8d\x2b\x3d\xf4\xcc\x81\xcb\x4e\xf0"
);
$this->assertSame(
Util::keyed_hash('Large Hashron Collider', $key),
'4cca9839943964a68a64535ea22f1cc796df6da130619a69d1022b84ef881881'
);
try {
Util::keyed_hash('Large Hashron Collider', $key, 15);
$this->fail('Invalid size accepted (15)');
} catch (CannotPerformOperation $ex) {
$this->assertSame(
'Output length must be at least 16 bytes.',
$ex->getMessage()
);
}
try {
Util::keyed_hash('Large Hashron Collider', $key, 65);
$this->fail('Invalid size accepted (65)');
} catch (CannotPerformOperation $ex) {
$this->assertSame(
'Output length must be at most 64 bytes.',
$ex->getMessage()
);
}
}
/**
* Test our HKDF-esque construct built atop BLAKE2b
*
* @throws CannotPerformOperation
* @throws TypeError
* @throws InvalidDigestLength
*/
public function testBlake2bKDF()
{
$ikm = 'YELLOW SUBMARINE';
$len = 32;
$info = 'TESTING HKDF-BLAKE2B';
$salt = str_repeat("\x80", 32);
$test = Util::hkdfBlake2b($ikm, $len, $info, $salt);
$this->assertSame(
$test,
"\x7b\xaf\xb1\x11\x1c\xda\xce\x81\xd1\xb0\x73\xff\x6e\x68\x8f\xc3".
"\x6f\xb5\xa2\xc7\xbd\x53\xf6\xf1\xb4\x2f\x80\x71\x29\x4b\xb7\xf7"
);
// Let's change the IKM
$ikmB = 'YELLOW SUBMARINF';
$testIkm = Util::hkdfBlake2b($ikmB, $len, $info, $salt);
$this->assertNotEquals($test, $testIkm);
// Let's change the info
$infoB = 'TESTING HKDF-BLAKE2C';
$testInfo = Util::hkdfBlake2b($ikm, $len, $infoB, $salt);
$this->assertNotEquals($test, $testInfo);
// Let's change the salt
$saltB = str_repeat("\x80", 31) . "\x81";
$testSalt = Util::hkdfBlake2b($ikm, $len, $info, $saltB);
$this->assertNotEquals($test, $testSalt);
try {
Util::hkdfBlake2b($ikm, -1, $info, $saltB);
$this->fail('Invalid digest length was accepted');
} catch (\ParagonIE\Halite\Alerts\InvalidDigestLength $ex) {
}
}
/**
* Verify that safeStrcpy() doesn't fall prey to interned strings.
*
* @throws Exception
* @throws TypeError
*/
public function testSafeStrcpy()
{
$unique = random_bytes(128);
$clone = Util::safeStrcpy($unique);
$this->assertSame($unique, $clone);
sodium_memzero($unique);
$this->assertNotSame($unique, $clone);
// See issue #97
$unique = random_bytes(1);
$clone = Util::safeStrcpy($unique);
$this->assertSame($unique, $clone);
sodium_memzero($unique);
$this->assertNotSame($unique, $clone);
}
/**
* Verify that xorStrings() produces the expected result.
*
* @throws InvalidType
*/
public function testXorStrings()
{
$this->assertSame('', Util::xorStrings('', ''));
$a = str_repeat("\x0f", 32);
$b = str_repeat("\x88", 32);
$this->assertSame(
str_repeat("\x87", 32),
Util::xorStrings($a, $b)
);
try {
$a .= "\x00";
$this->assertSame(
str_repeat("\x87", 32),
Util::xorStrings($a, $b)
);
$this->fail('Incorrect string length should throw an exception.');
} catch (InvalidType $ex) {
}
}
}
test/unit/FileTest.php 0000666 00000064132 13536431632 0010752 0 ustar 00 assertSame(
hash_file('sha256', __DIR__.'/tmp/paragon_avatar.png'),
hash_file('sha256', __DIR__.'/tmp/paragon_avatar.decrypted.png')
);
unlink(__DIR__.'/tmp/paragon_avatar.encrypted.png');
unlink(__DIR__.'/tmp/paragon_avatar.decrypted.png');
}
/**
* @throws CryptoException\CannotPerformOperation
* @throws CryptoException\FileAccessDenied
* @throws CryptoException\FileError
* @throws CryptoException\FileModified
* @throws CryptoException\InvalidDigestLength
* @throws CryptoException\InvalidKey
* @throws CryptoException\InvalidMessage
* @throws CryptoException\InvalidType
* @throws TypeError
*/
public function testEncryptEmpty()
{
file_put_contents(__DIR__.'/tmp/empty.txt', '');
chmod(__DIR__.'/tmp/empty.txt', 0777);
touch(__DIR__.'/tmp/empty.encrypted.txt');
chmod(__DIR__.'/tmp/empty.encrypted.txt', 0777);
touch(__DIR__.'/tmp/empty.decrypted.txt');
chmod(__DIR__.'/tmp/empty.decrypted.txt', 0777);
$key = new EncryptionKey(
new HiddenString(\str_repeat('B', 32))
);
File::encrypt(
__DIR__.'/tmp/empty.txt',
__DIR__.'/tmp/empty.encrypted.txt',
$key
);
File::decrypt(
__DIR__.'/tmp/empty.encrypted.txt',
__DIR__.'/tmp/empty.decrypted.txt',
$key
);
$this->assertSame(
hash_file('sha256', __DIR__.'/tmp/empty.txt'),
hash_file('sha256', __DIR__.'/tmp/empty.decrypted.txt')
);
unlink(__DIR__.'/tmp/empty.txt');
unlink(__DIR__.'/tmp/empty.encrypted.txt');
unlink(__DIR__.'/tmp/empty.decrypted.txt');
}
/**
* @throws CryptoException\CannotPerformOperation
* @throws CryptoException\FileAccessDenied
* @throws CryptoException\FileError
* @throws CryptoException\FileModified
* @throws CryptoException\InvalidDigestLength
* @throws CryptoException\InvalidKey
* @throws CryptoException\InvalidMessage
* @throws CryptoException\InvalidType
* @throws Exception
* @throws TypeError
*/
public function testEncryptFail()
{
touch(__DIR__.'/tmp/paragon_avatar.encrypt_fail.png');
chmod(__DIR__.'/tmp/paragon_avatar.encrypt_fail.png', 0777);
touch(__DIR__.'/tmp/paragon_avatar.decrypt_fail.png');
chmod(__DIR__.'/tmp/paragon_avatar.decrypt_fail.png', 0777);
$key = new EncryptionKey(
new HiddenString(\str_repeat('B', 32))
);
File::encrypt(
__DIR__.'/tmp/paragon_avatar.png',
__DIR__.'/tmp/paragon_avatar.encrypt_fail.png',
$key
);
$fp = fopen(__DIR__.'/tmp/paragon_avatar.encrypt_fail.png', 'ab');
fwrite($fp, random_bytes(1));
fclose($fp);
try {
File::decrypt(
__DIR__.'/tmp/paragon_avatar.encrypt_fail.png',
__DIR__.'/tmp/paragon_avatar.decrypt_fail.png',
$key
);
$this->fail(
'This should have thrown an InvalidMessage exception!'
);
} catch (CryptoException\InvalidMessage $e) {
$this->assertTrue($e instanceof CryptoException\InvalidMessage);
unlink(__DIR__.'/tmp/paragon_avatar.encrypt_fail.png');
unlink(__DIR__.'/tmp/paragon_avatar.decrypt_fail.png');
}
try {
File::encrypt(true, false, $key);
$this->fail('Invalid type was accepted.');
} catch (CryptoException\InvalidType $ex) {
}
try {
File::decrypt(true, false, $key);
$this->fail('Invalid type was accepted.');
} catch (CryptoException\InvalidType $ex) {
}
}
/**
* @throws CryptoException\CannotPerformOperation
* @throws CryptoException\FileAccessDenied
* @throws CryptoException\FileError
* @throws CryptoException\FileModified
* @throws CryptoException\InvalidDigestLength
* @throws CryptoException\InvalidKey
* @throws CryptoException\InvalidType
* @throws TypeError
*/
public function testEncryptSmallFail()
{
touch(__DIR__.'/tmp/empty.encrypted.txt');
chmod(__DIR__.'/tmp/empty.encrypted.txt', 0777);
touch(__DIR__.'/tmp/empty.decrypted.txt');
chmod(__DIR__.'/tmp/empty.decrypted.txt', 0777);
$msg = 'Input file is too small to have been encrypted by Halite.';
$key = new EncryptionKey(
new HiddenString(str_repeat('B', 32))
);
file_put_contents(
__DIR__.'/tmp/empty.encrypted.txt',
''
);
try {
File::decrypt(
__DIR__ . '/tmp/empty.encrypted.txt',
__DIR__ . '/tmp/empty.decrypted.txt',
$key
);
$this->fail("This should scream bloody murder");
} catch (CryptoException\InvalidMessage $e) {
$this->assertSame($msg, $e->getMessage());
}
file_put_contents(
__DIR__.'/tmp/empty.encrypted.txt',
"\x31\x41\x03\x00\x01"
);
try {
File::decrypt(
__DIR__ . '/tmp/empty.encrypted.txt',
__DIR__ . '/tmp/empty.decrypted.txt',
$key
);
$this->fail("This should scream bloody murder");
} catch (CryptoException\InvalidMessage $e) {
$this->assertSame($msg, $e->getMessage());
}
file_put_contents(
__DIR__.'/tmp/empty.encrypted.txt',
"\x31\x41\x03\x00" . \str_repeat("\x00", 87)
);
try {
File::decrypt(
__DIR__ . '/tmp/empty.encrypted.txt',
__DIR__ . '/tmp/empty.decrypted.txt',
$key
);
$this->fail("This should scream bloody murder");
} catch (CryptoException\InvalidMessage $e) {
$this->assertSame($msg, $e->getMessage());
}
unlink(__DIR__.'/tmp/empty.encrypted.txt');
unlink(__DIR__.'/tmp/empty.decrypted.txt');
}
/**
* @throws CryptoException\CannotPerformOperation
* @throws CryptoException\FileAccessDenied
* @throws CryptoException\FileError
* @throws CryptoException\FileModified
* @throws CryptoException\InvalidDigestLength
* @throws CryptoException\InvalidKey
* @throws CryptoException\InvalidMessage
* @throws CryptoException\InvalidType
* @throws TypeError
*/
public function testEncryptVarious()
{
touch(__DIR__.'/tmp/paragon_avatar.encrypted.png');
chmod(__DIR__.'/tmp/paragon_avatar.encrypted.png', 0777);
touch(__DIR__.'/tmp/paragon_avatar.decrypted.png');
chmod(__DIR__.'/tmp/paragon_avatar.decrypted.png', 0777);
$key = new EncryptionKey(
new HiddenString(\str_repeat('B', 32))
);
File::encrypt(
new ReadOnlyFile(__DIR__.'/tmp/paragon_avatar.png'),
new MutableFile(__DIR__.'/tmp/paragon_avatar.encrypted.png'),
$key
);
File::decrypt(
new ReadOnlyFile(__DIR__.'/tmp/paragon_avatar.encrypted.png'),
new MutableFile(__DIR__.'/tmp/paragon_avatar.decrypted.png'),
$key
);
$this->assertSame(
hash_file('sha256', __DIR__.'/tmp/paragon_avatar.png'),
hash_file('sha256', __DIR__.'/tmp/paragon_avatar.decrypted.png')
);
File::encrypt(
new WeakReadOnlyFile(__DIR__.'/tmp/paragon_avatar.png'),
new MutableFile(__DIR__.'/tmp/paragon_avatar.encrypted.png'),
$key
);
File::decrypt(
new WeakReadOnlyFile(__DIR__.'/tmp/paragon_avatar.encrypted.png'),
new MutableFile(__DIR__.'/tmp/paragon_avatar.decrypted.png'),
$key
);
$this->assertSame(
hash_file('sha256', __DIR__.'/tmp/paragon_avatar.png'),
hash_file('sha256', __DIR__.'/tmp/paragon_avatar.decrypted.png')
);
unlink(__DIR__.'/tmp/paragon_avatar.encrypted.png');
unlink(__DIR__.'/tmp/paragon_avatar.decrypted.png');
}
/**
* @throws CryptoException\CannotPerformOperation
* @throws CryptoException\FileAccessDenied
* @throws CryptoException\FileError
* @throws CryptoException\FileModified
* @throws CryptoException\InvalidDigestLength
* @throws CryptoException\InvalidMessage
* @throws CryptoException\InvalidType
* @throws Exception
* @throws TypeError
*/
public function testSeal()
{
touch(__DIR__.'/tmp/paragon_avatar.sealed.png');
chmod(__DIR__.'/tmp/paragon_avatar.sealed.png', 0777);
touch(__DIR__.'/tmp/paragon_avatar.opened.png');
chmod(__DIR__.'/tmp/paragon_avatar.opened.png', 0777);
$keypair = KeyFactory::generateEncryptionKeyPair();
$secretkey = $keypair->getSecretKey();
$publickey = $keypair->getPublicKey();
File::seal(
__DIR__.'/tmp/paragon_avatar.png',
__DIR__.'/tmp/paragon_avatar.sealed.png',
$publickey
);
File::unseal(
__DIR__.'/tmp/paragon_avatar.sealed.png',
__DIR__.'/tmp/paragon_avatar.opened.png',
$secretkey
);
$this->assertSame(
hash_file('sha256', __DIR__.'/tmp/paragon_avatar.png'),
hash_file('sha256', __DIR__.'/tmp/paragon_avatar.opened.png')
);
unlink(__DIR__.'/tmp/paragon_avatar.sealed.png');
unlink(__DIR__.'/tmp/paragon_avatar.opened.png');
}
/**
* @throws CryptoException\CannotPerformOperation
* @throws CryptoException\FileAccessDenied
* @throws CryptoException\FileError
* @throws CryptoException\FileModified
* @throws CryptoException\InvalidDigestLength
* @throws CryptoException\InvalidMessage
* @throws CryptoException\InvalidType
* @throws Exception
* @throws TypeError
*/
public function testSealFromStreamWrapper()
{
require_once __DIR__ . '/RemoteStream.php';
stream_register_wrapper('haliteTest', RemoteStream::class);
touch(__DIR__.'/tmp/paragon_avatar.sealed.png');
chmod(__DIR__.'/tmp/paragon_avatar.sealed.png', 0777);
touch(__DIR__.'/tmp/paragon_avatar.opened.png');
chmod(__DIR__.'/tmp/paragon_avatar.opened.png', 0777);
$keypair = KeyFactory::generateEncryptionKeyPair();
$secretkey = $keypair->getSecretKey();
$publickey = $keypair->getPublicKey();
$file = new ReadOnlyFile(fopen('haliteTest://paragon_avatar.png', 'rb'));
File::seal(
$file,
__DIR__.'/tmp/paragon_avatar.sealed.png',
$publickey
);
File::unseal(
__DIR__.'/tmp/paragon_avatar.sealed.png',
__DIR__.'/tmp/paragon_avatar.opened.png',
$secretkey
);
$this->assertSame(
hash_file('sha256', __DIR__.'/tmp/paragon_avatar.png'),
hash_file('sha256', __DIR__.'/tmp/paragon_avatar.opened.png')
);
unlink(__DIR__.'/tmp/paragon_avatar.sealed.png');
unlink(__DIR__.'/tmp/paragon_avatar.opened.png');
$this->assertEquals($file->getHash(), (new ReadOnlyFile(__DIR__.'/tmp/paragon_avatar.png'))->getHash());
}
/**
* @throws CryptoException\CannotPerformOperation
* @throws CryptoException\FileAccessDenied
* @throws CryptoException\FileError
* @throws CryptoException\FileModified
* @throws CryptoException\InvalidDigestLength
* @throws CryptoException\InvalidMessage
* @throws CryptoException\InvalidType
* @throws Exception
* @throws TypeError
*/
public function testSealEmpty()
{
file_put_contents(__DIR__.'/tmp/empty.txt', '');
chmod(__DIR__.'/tmp/empty.txt', 0777);
touch(__DIR__.'/tmp/empty.sealed.txt');
chmod(__DIR__.'/tmp/empty.sealed.txt', 0777);
touch(__DIR__.'/tmp/empty.unsealed.txt');
chmod(__DIR__.'/tmp/empty.unsealed.txt', 0777);
$keypair = KeyFactory::generateEncryptionKeyPair();
$secretkey = $keypair->getSecretKey();
$publickey = $keypair->getPublicKey();
File::seal(
__DIR__.'/tmp/empty.txt',
__DIR__.'/tmp/empty.sealed.txt',
$publickey
);
File::unseal(
__DIR__.'/tmp/empty.sealed.txt',
__DIR__.'/tmp/empty.unsealed.txt',
$secretkey
);
$this->assertSame(
hash_file('sha256', __DIR__.'/tmp/empty.txt'),
hash_file('sha256', __DIR__.'/tmp/empty.unsealed.txt')
);
unlink(__DIR__.'/tmp/empty.txt');
unlink(__DIR__.'/tmp/empty.sealed.txt');
unlink(__DIR__.'/tmp/empty.unsealed.txt');
}
/**
* @throws CryptoException\CannotPerformOperation
* @throws CryptoException\FileAccessDenied
* @throws CryptoException\FileError
* @throws CryptoException\FileModified
* @throws CryptoException\InvalidDigestLength
* @throws CryptoException\InvalidMessage
* @throws CryptoException\InvalidType
* @throws Exception
* @throws TypeError
*/
public function testSealFail()
{
touch(__DIR__.'/tmp/paragon_avatar.seal_fail.png');
chmod(__DIR__.'/tmp/paragon_avatar.seal_fail.png', 0777);
touch(__DIR__.'/tmp/paragon_avatar.open_fail.png');
chmod(__DIR__.'/tmp/paragon_avatar.open_fail.png', 0777);
$keypair = KeyFactory::generateEncryptionKeyPair();
$secretkey = $keypair->getSecretKey();
$publickey = $keypair->getPublicKey();
File::seal(
__DIR__.'/tmp/paragon_avatar.png',
__DIR__.'/tmp/paragon_avatar.seal_fail.png',
$publickey
);
$fp = fopen(__DIR__.'/tmp/paragon_avatar.seal_fail.png', 'ab');
fwrite($fp, random_bytes(1));
fclose($fp);
try {
File::unseal(
__DIR__.'/tmp/paragon_avatar.seal_fail.png',
__DIR__.'/tmp/paragon_avatar.open_fail.png',
$secretkey
);
$this->fail(
'This should have thrown an InvalidMessage exception!'
);
} catch (CryptoException\InvalidMessage $e) {
$this->assertTrue($e instanceof CryptoException\InvalidMessage);
unlink(__DIR__.'/tmp/paragon_avatar.seal_fail.png');
unlink(__DIR__.'/tmp/paragon_avatar.open_fail.png');
}
try {
File::seal(true, false, $publickey);
$this->fail('Invalid type was accepted.');
} catch (CryptoException\InvalidType $ex) {
}
try {
File::unseal(true, false, $secretkey);
$this->fail('Invalid type was accepted.');
} catch (CryptoException\InvalidType $ex) {
}
}
/**
* @throws CryptoException\CannotPerformOperation
* @throws CryptoException\FileAccessDenied
* @throws CryptoException\FileError
* @throws CryptoException\FileModified
* @throws CryptoException\InvalidDigestLength
* @throws CryptoException\InvalidKey
* @throws CryptoException\InvalidType
* @throws TypeError
*/
public function testSealSmallFail()
{
touch(__DIR__.'/tmp/empty.sealed.txt');
chmod(__DIR__.'/tmp/empty.sealed.txt', 0777);
touch(__DIR__.'/tmp/empty.unsealed.txt');
chmod(__DIR__.'/tmp/empty.unsealed.txt', 0777);
$msg = 'Input file is too small to have been encrypted by Halite.';
$keypair = KeyFactory::generateEncryptionKeyPair();
$secretkey = $keypair->getSecretKey();
file_put_contents(__DIR__.'/tmp/empty.sealed.txt', '');
try {
File::unseal(
__DIR__.'/tmp/empty.sealed.txt',
__DIR__.'/tmp/empty.unsealed.txt',
$secretkey
);
$this->fail("This should scream bloody murder");
} catch (CryptoException\InvalidMessage $e) {
$this->assertSame($msg, $e->getMessage());
}
file_put_contents(
__DIR__.'/tmp/empty.sealed.txt',
"\x31\x41\x03\x00" . \str_repeat("\x00", 95)
);
try {
File::unseal(
__DIR__.'/tmp/empty.sealed.txt',
__DIR__.'/tmp/empty.unsealed.txt',
$secretkey
);
$this->fail("This should scream bloody murder");
} catch (CryptoException\InvalidMessage $e) {
$this->assertSame($msg, $e->getMessage());
}
unlink(__DIR__.'/tmp/empty.sealed.txt');
unlink(__DIR__.'/tmp/empty.unsealed.txt');
}
/**
* @throws CryptoException\CannotPerformOperation
* @throws CryptoException\FileAccessDenied
* @throws CryptoException\FileError
* @throws CryptoException\FileModified
* @throws CryptoException\InvalidDigestLength
* @throws CryptoException\InvalidMessage
* @throws CryptoException\InvalidType
* @throws Exception
* @throws TypeError
*/
public function testSealVarious()
{
touch(__DIR__.'/tmp/paragon_avatar.sealed.png');
chmod(__DIR__.'/tmp/paragon_avatar.sealed.png', 0777);
touch(__DIR__.'/tmp/paragon_avatar.opened.png');
chmod(__DIR__.'/tmp/paragon_avatar.opened.png', 0777);
$keypair = KeyFactory::generateEncryptionKeyPair();
$secretkey = $keypair->getSecretKey();
$publickey = $keypair->getPublicKey();
File::seal(
new ReadOnlyFile(__DIR__.'/tmp/paragon_avatar.png'),
new MutableFile(__DIR__.'/tmp/paragon_avatar.sealed.png'),
$publickey
);
File::unseal(
new ReadOnlyFile(__DIR__.'/tmp/paragon_avatar.sealed.png'),
new MutableFile(__DIR__.'/tmp/paragon_avatar.opened.png'),
$secretkey
);
$this->assertSame(
hash_file('sha256', __DIR__.'/tmp/paragon_avatar.png'),
hash_file('sha256', __DIR__.'/tmp/paragon_avatar.opened.png')
);
File::seal(
new WeakReadOnlyFile(__DIR__.'/tmp/paragon_avatar.png'),
new MutableFile(__DIR__.'/tmp/paragon_avatar.sealed.png'),
$publickey
);
File::unseal(
new WeakReadOnlyFile(__DIR__.'/tmp/paragon_avatar.sealed.png'),
new MutableFile(__DIR__.'/tmp/paragon_avatar.opened.png'),
$secretkey
);
$this->assertSame(
hash_file('sha256', __DIR__.'/tmp/paragon_avatar.png'),
hash_file('sha256', __DIR__.'/tmp/paragon_avatar.opened.png')
);
unlink(__DIR__.'/tmp/paragon_avatar.sealed.png');
unlink(__DIR__.'/tmp/paragon_avatar.opened.png');
}
/**
* @throws CryptoException\CannotPerformOperation
* @throws CryptoException\FileAccessDenied
* @throws CryptoException\FileError
* @throws CryptoException\InvalidKey
* @throws CryptoException\InvalidMessage
* @throws CryptoException\InvalidSignature
* @throws CryptoException\InvalidType
* @throws TypeError
*/
public function testSign()
{
$keypair = KeyFactory::generateSignatureKeyPair();
$secretkey = $keypair->getSecretKey();
$publickey = $keypair->getPublicKey();
$signature = File::sign(
__DIR__.'/tmp/paragon_avatar.png',
$secretkey
);
$this->assertTrue(
File::verify(
__DIR__.'/tmp/paragon_avatar.png',
$publickey,
$signature
)
);
try {
File::sign(true, $secretkey);
$this->fail('Invalid type was accepted.');
} catch (CryptoException\InvalidType $ex) {
}
try {
File::verify(false, $publickey, '');
$this->fail('Invalid type was accepted.');
} catch (CryptoException\InvalidType $ex) {
}
}
/**
* @throws CryptoException\CannotPerformOperation
* @throws CryptoException\FileAccessDenied
* @throws CryptoException\FileError
* @throws CryptoException\InvalidKey
* @throws CryptoException\InvalidMessage
* @throws CryptoException\InvalidSignature
* @throws CryptoException\InvalidType
* @throws TypeError
*/
public function testSignVarious()
{
$keypair = KeyFactory::generateSignatureKeyPair();
$secretkey = $keypair->getSecretKey();
$publickey = $keypair->getPublicKey();
$inputFile = new ReadOnlyFile(__DIR__.'/tmp/paragon_avatar.png');
$signature = File::sign(
$inputFile,
$secretkey
);
$this->assertTrue(
File::verify(
$inputFile,
$publickey,
$signature
)
);
$mutable = new WeakReadOnlyFile(__DIR__.'/tmp/paragon_avatar.png');
$signature = File::sign(
$mutable,
$secretkey
);
$this->assertTrue(
File::verify(
$mutable,
$publickey,
$signature
)
);
}
/**
* @throws CryptoException\CannotPerformOperation
* @throws CryptoException\FileAccessDenied
* @throws CryptoException\FileError
* @throws CryptoException\InvalidKey
* @throws CryptoException\InvalidMessage
* @throws CryptoException\InvalidType
* @throws Exception
* @throws TypeError
*/
public function testChecksum()
{
$csum = File::checksum(__DIR__.'/tmp/paragon_avatar.png', null, false);
$this->assertSame(
$csum,
"09f9f74a0e742d057ca08394db4c2e444be88c0c94fe9a914c3d3758c7eccafb".
"8dd286e3d6bc37f353e76c0c5aa2036d978ca28ffaccfa59f5dc1f076c5517a0"
);
$data = random_bytes(32);
file_put_contents(__DIR__.'/tmp/garbage.dat', $data);
$hash = Util::raw_hash($data, 64);
$file = File::checksum(__DIR__.'/tmp/garbage.dat', null, true);
$this->assertSame(
$hash,
$file
);
$this->assertSame(
$hash,
File::checksum(new ReadOnlyFile(__DIR__.'/tmp/garbage.dat'), null, true)
);
$this->assertSame(
$hash,
File::checksum(new WeakReadOnlyFile(__DIR__.'/tmp/garbage.dat'), null, true)
);
// No exceptions:
File::checksum(__DIR__.'/tmp/garbage.dat', KeyFactory::generateAuthenticationKey(), true);
File::checksum(__DIR__.'/tmp/garbage.dat', KeyFactory::generateSignatureKeyPair()->getPublicKey(), true);
try {
File::checksum(false);
$this->fail('Invalid type was accepted.');
} catch (CryptoException\InvalidType $ex) {
}
try {
File::checksum(__DIR__.'/tmp/garbage.dat', KeyFactory::generateEncryptionKey());
$this->fail('Invalid type was accepted.');
} catch (CryptoException\InvalidKey $ex) {
}
unlink(__DIR__.'/tmp/garbage.dat');
}
public function testNonExistingOutputFile()
{
file_put_contents(__DIR__.'/tmp/empty116.txt', '');
if (\is_file(__DIR__ . '/tmp/empty116.encrypted.txt')) {
\unlink(__DIR__ . '/tmp/empty116.encrypted.txt');
\clearstatcache();
}
$key = new EncryptionKey(
new HiddenString(\str_repeat('B', 32))
);
File::encrypt(
__DIR__.'/tmp/empty116.txt',
__DIR__.'/tmp/empty116.encrypted.txt',
$key
);
$this->assertTrue(\file_exists(__DIR__.'/tmp/empty116.encrypted.txt'));
}
public function testOutputToOutputbuffer()
{
$stream = fopen('php://output', 'wb');
touch(__DIR__.'/tmp/paragon_avatar.encrypted.png');
chmod(__DIR__.'/tmp/paragon_avatar.encrypted.png', 0777);
$key = new EncryptionKey(
new HiddenString(\str_repeat('B', 32))
);
File::encrypt(
__DIR__.'/tmp/paragon_avatar.png',
__DIR__.'/tmp/paragon_avatar.encrypted.png',
$key
);
ob_start();
File::decrypt(
__DIR__.'/tmp/paragon_avatar.encrypted.png',
$stream,
$key
);
$contents = ob_get_clean();
$this->assertSame(
hash_file('sha256', __DIR__.'/tmp/paragon_avatar.png'),
hash('sha256', $contents)
);
unlink(__DIR__.'/tmp/paragon_avatar.encrypted.png');
}
}
test/unit/KeyPairTest.php 0000666 00000030234 13536431632 0011433 0 ustar 00 getSecretKey();
$sign_public = $keypair->getPublicKey();
$this->assertTrue($sign_secret instanceof SignatureSecretKey);
$this->assertTrue($sign_public instanceof SignaturePublicKey);
// Can this be used?
$message = 'This is a test message';
$signed = Asymmetric::sign(
$message,
$sign_secret
);
$this->assertTrue(
Asymmetric::verify(
$message,
$sign_public,
$signed
)
);
$this->assertSame(
$sign_public->getRawKeyMaterial(),
"\x9a\xce\x92\x8f\x6a\x27\x93\x8e\x87\xac\x9b\x97\xfb\xe2\x50\x6b" .
"\x67\xd5\x8b\x68\xeb\x37\xc2\x2d\x31\xdb\xcf\x7e\x8d\xa0\xcb\x17",
KeyFactory::INTERACTIVE
);
}
/**
* @throws TypeError
* @throws CryptoException\InvalidKey
* @throws CryptoException\InvalidSalt
* @throws CryptoException\InvalidSignature
* @throws CryptoException\InvalidType
*/
public function testDeriveSigningKeyOldArgon2i()
{
$keypair = KeyFactory::deriveSignatureKeyPair(
new HiddenString('apple'),
"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f",
KeyFactory::INTERACTIVE,
SODIUM_CRYPTO_PWHASH_ALG_ARGON2I13
);
$sign_secret = $keypair->getSecretKey();
$sign_public = $keypair->getPublicKey();
$this->assertTrue($sign_secret instanceof SignatureSecretKey);
$this->assertTrue($sign_public instanceof SignaturePublicKey);
// Can this be used?
$message = 'This is a test message';
$signed = Asymmetric::sign(
$message,
$sign_secret
);
$this->assertTrue(
Asymmetric::verify(
$message,
$sign_public,
$signed
)
);
$this->assertSame(
$sign_public->getRawKeyMaterial(),
"\x88\x9c\xc0\x7a\x90\xb8\x98\xf4\x6b\x47\xfe\xcc\x91\x42\x58\x45".
"\x41\xcf\x4b\x5c\x6a\x82\x2d\xdc\xc6\x8b\x87\xbc\x08\x2f\xfe\x95"
);
}
/**
* @throws TypeError
* @throws CryptoException\CannotPerformOperation
* @throws CryptoException\InvalidKey
*/
public function testEncryptionKeyPair()
{
$boxKeypair = KeyFactory::generateEncryptionKeyPair();
$boxSecret = $boxKeypair->getSecretKey();
$boxPublic = $boxKeypair->getPublicKey();
$this->assertInstanceOf(\ParagonIE\Halite\Asymmetric\SecretKey::class, $boxSecret);
$this->assertInstanceOf(\ParagonIE\Halite\Asymmetric\PublicKey::class, $boxPublic);
$second = new EncryptionKeyPair(
$boxPublic,
$boxSecret
);
$this->assertSame(
Hex::encode($boxSecret->getRawKeyMaterial()),
Hex::encode($second->getSecretKey()->getRawKeyMaterial()),
'Secret keys differ'
);
$this->assertSame(
Hex::encode($boxPublic->getRawKeyMaterial()),
Hex::encode($second->getPublicKey()->getRawKeyMaterial()),
'Public keys differ'
);
$third = new EncryptionKeyPair(
$boxSecret,
$boxPublic
);
$this->assertSame(
Hex::encode($boxSecret->getRawKeyMaterial()),
Hex::encode($third->getSecretKey()->getRawKeyMaterial()),
'Secret keys differ'
);
$this->assertSame(
Hex::encode($boxPublic->getRawKeyMaterial()),
Hex::encode($third->getPublicKey()->getRawKeyMaterial()),
'Public keys differ'
);
$fourth = new EncryptionKeyPair(
$boxSecret
);
$this->assertSame(
Hex::encode($boxSecret->getRawKeyMaterial()),
Hex::encode($fourth->getSecretKey()->getRawKeyMaterial()),
'Secret keys differ'
);
$this->assertSame(
Hex::encode($boxPublic->getRawKeyMaterial()),
Hex::encode($fourth->getPublicKey()->getRawKeyMaterial()),
'Public keys differ'
);
try {
new EncryptionKeyPair(
$boxSecret,
$boxPublic,
$boxPublic
);
$this->fail('More than two public keys was erroneously accepted');
} catch (\InvalidArgumentException $ex) {
}
try {
new EncryptionKeyPair(
$boxPublic
);
$this->fail('Two public keys was erroneously accepted');
} catch (\ParagonIE\Halite\Alerts\InvalidKey $ex) {
}
try {
new EncryptionKeyPair(
KeyFactory::generateEncryptionKey()
);
$this->fail('Symmetric key was erroneously accepted');
} catch (\ParagonIE\Halite\Alerts\InvalidKey $ex) {
}
try {
new EncryptionKeyPair(
$boxSecret,
KeyFactory::generateEncryptionKey()
);
$this->fail('Symmetric key was erroneously accepted');
} catch (\ParagonIE\Halite\Alerts\InvalidKey $ex) {
}
try {
new EncryptionKeyPair(
$boxSecret,
$boxSecret
);
$this->fail('Two secret keys was erroneously accepted');
} catch (\ParagonIE\Halite\Alerts\InvalidKey $ex) {
}
try {
new EncryptionKeyPair(
$boxPublic,
$boxPublic
);
$this->fail('Two public keys was erroneously accepted');
} catch (\ParagonIE\Halite\Alerts\InvalidKey $ex) {
}
}
/**
* @throws TypeError
* @throws CryptoException\CannotPerformOperation
* @throws CryptoException\InvalidKey
*/
public function testFileStorage()
{
$filename = tempnam(__DIR__.'/tmp/', 'key');
$key = KeyFactory::generateEncryptionKeyPair();
KeyFactory::save($key, $filename);
$copy = KeyFactory::loadEncryptionKeyPair($filename);
$this->assertSame(
$key->getPublicKey()->getRawKeyMaterial(),
$copy->getPublicKey()->getRawKeyMaterial()
);
unlink($filename);
}
/**
* @throws TypeError
* @throws CryptoException\InvalidKey
*/
public function testMutation()
{
$sign_kp = KeyFactory::generateSignatureKeyPair();
$box_kp = $sign_kp->getEncryptionKeyPair();
$sign_sk = $sign_kp->getSecretKey();
$sign_pk = $sign_kp->getPublicKey();
$enc_sk = $sign_sk->getEncryptionSecretKey();
$enc_pk = $sign_pk->getEncryptionPublicKey();
$this->assertSame(
Hex::encode($enc_pk->getRawKeyMaterial()),
Hex::encode($enc_sk->derivePublicKey()->getRawKeyMaterial())
);
$this->assertSame(
Hex::encode($enc_sk->getRawKeyMaterial()),
Hex::encode($box_kp->getSecretKey()->getRawKeyMaterial())
);
}
/**
* @throws TypeError
* @throws CryptoException\CannotPerformOperation
* @throws CryptoException\InvalidKey
*/
public function testSignatureKeyPair()
{
$signKeypair = KeyFactory::generateSignatureKeyPair();
$signSecret = $signKeypair->getSecretKey();
$signPublic = $signKeypair->getPublicKey();
$this->assertInstanceOf(\ParagonIE\Halite\Asymmetric\SecretKey::class, $signSecret);
$this->assertInstanceOf(\ParagonIE\Halite\Asymmetric\PublicKey::class, $signPublic);
$second = new SignatureKeyPair(
$signPublic,
$signSecret
);
$this->assertSame(
Hex::encode($signSecret->getRawKeyMaterial()),
Hex::encode($second->getSecretKey()->getRawKeyMaterial()),
'Secret keys differ'
);
$this->assertSame(
Hex::encode($signPublic->getRawKeyMaterial()),
Hex::encode($second->getPublicKey()->getRawKeyMaterial()),
'Public keys differ'
);
$third = new SignatureKeyPair(
$signSecret,
$signPublic
);
$this->assertSame(
Hex::encode($signSecret->getRawKeyMaterial()),
Hex::encode($third->getSecretKey()->getRawKeyMaterial()),
'Secret keys differ'
);
$this->assertSame(
Hex::encode($signPublic->getRawKeyMaterial()),
Hex::encode($third->getPublicKey()->getRawKeyMaterial()),
'Public keys differ'
);
$fourth = new SignatureKeyPair(
$signSecret
);
$this->assertSame(
Hex::encode($signSecret->getRawKeyMaterial()),
Hex::encode($fourth->getSecretKey()->getRawKeyMaterial()),
'Secret keys differ'
);
$this->assertSame(
Hex::encode($signPublic->getRawKeyMaterial()),
Hex::encode($fourth->getPublicKey()->getRawKeyMaterial()),
'Public keys differ'
);
try {
new SignatureKeyPair(
$signSecret,
$signPublic,
$signPublic
);
$this->fail('More than two public keys was erroneously accepted');
} catch (\InvalidArgumentException $ex) {
}
try {
new SignatureKeyPair(
$signPublic
);
$this->fail('Two public keys was erroneously accepted');
} catch (\ParagonIE\Halite\Alerts\InvalidKey $ex) {
}
try {
new SignatureKeyPair(
KeyFactory::generateAuthenticationKey()
);
$this->fail('Symmetric key was erroneously accepted');
} catch (\ParagonIE\Halite\Alerts\InvalidKey $ex) {
}
try {
new SignatureKeyPair(
$signSecret,
KeyFactory::generateAuthenticationKey()
);
$this->fail('Symmetric key was erroneously accepted');
} catch (\ParagonIE\Halite\Alerts\InvalidKey $ex) {
}
try {
new SignatureKeyPair(
$signSecret,
$signSecret
);
$this->fail('Two secret keys was erroneously accepted');
} catch (\ParagonIE\Halite\Alerts\InvalidKey $ex) {
}
try {
new SignatureKeyPair(
$signPublic,
$signPublic
);
$this->fail('Two public keys was erroneously accepted');
} catch (\ParagonIE\Halite\Alerts\InvalidKey $ex) {
}
}
/**
* @throws TypeError
* @throws CryptoException\InvalidKey
*/
public function testPublicDerivation()
{
$enc_kp = KeyFactory::generateEncryptionKeyPair();
$enc_secret = $enc_kp->getSecretKey();
$enc_public = $enc_kp->getPublicKey();
$this->assertSame(
$enc_secret->derivePublicKey()->getRawKeyMaterial(),
$enc_public->getRawKeyMaterial()
);
$sign_kp = KeyFactory::generateSignatureKeyPair();
$sign_secret = $sign_kp->getSecretKey();
$sign_public = $sign_kp->getPublicKey();
$this->assertSame(
$sign_secret->derivePublicKey()->getRawKeyMaterial(),
$sign_public->getRawKeyMaterial()
);
}
}
test/unit/tmp/paragon_avatar.png 0000666 00000425056 13536431632 0013023 0 ustar 00 PNG
IHDR [Cw&