<?php
/**
* SeekQuarry/Yioop --
* Open Source Pure PHP Search Engine, Crawler, and Indexer
*
* Copyright (C) 2009 - 2026 Chris Pollett chris@pollett.org
*
* LICENSE:
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* END LICENSE
*
* @author Chris Pollett chris@pollett.org
* @license https://www.gnu.org/licenses/ GPL3
* @link https://www.seekquarry.com/
* @copyright 2009 - 2026
* @filesource
*/
namespace seekquarry\yioop\tests;
use seekquarry\yioop\library\CredentialCipher;
use seekquarry\yioop\library\UnitTest;
/**
* Tests for CredentialCipher, now a pure-crypto helper. Each test
* mints a fresh random master key in setUp and exercises encrypt /
* decrypt against it; no database is involved, since key storage now
* lives in MailAccountModel.
*
* @author Chris Pollett
*/
class CredentialCipherTest extends UnitTest
{
/**
* Master key minted fresh in setUp; the round-trip tests encrypt
* and decrypt against it.
* @var string
*/
public $key;
/**
* Mints a fresh random master key. CredentialCipher is pure crypto
* now, so no database is involved.
*/
public function setUp()
{
$this->key = random_bytes(SODIUM_CRYPTO_SECRETBOX_KEYBYTES);
}
/**
* Nothing to tear down; the cipher keeps no state between tests.
*/
public function tearDown()
{
}
/**
* encrypt then decrypt returns the original plaintext for both
* ASCII and binary payloads, including the empty string.
*/
public function roundTripTestCase()
{
foreach (["hello", "", "with spaces and \"quotes\"",
str_repeat("\x00\x01\x02", 50),
"unicode: café résumé naïve"] as $plaintext) {
$pair = CredentialCipher::encrypt($this->key, $plaintext);
$back = CredentialCipher::decrypt($this->key,
$pair['nonce'], $pair['ciphertext']);
$this->assertEqual($plaintext, $back,
"Round trip preserves " . strlen($plaintext) .
"-byte plaintext");
}
}
/**
* Two successive encrypts of the same plaintext produce different
* ciphertexts because each gets a fresh random nonce.
*/
public function nonceUniquenessTestCase()
{
$first = CredentialCipher::encrypt($this->key, "same input");
$second = CredentialCipher::encrypt($this->key, "same input");
$this->assertTrue($first['nonce'] !== $second['nonce'],
"Nonces differ between successive encrypts");
$this->assertTrue(
$first['ciphertext'] !== $second['ciphertext'],
"Ciphertexts differ between successive encrypts");
}
/**
* Decrypting a ciphertext whose payload has been altered by a
* single byte should throw \Exception.
*/
public function tamperDetectionTestCase()
{
$pair = CredentialCipher::encrypt($this->key, "secret");
$cipher_raw = base64_decode($pair['ciphertext']);
$cipher_raw[5] = chr(ord($cipher_raw[5]) ^ 0x01);
$tampered = base64_encode($cipher_raw);
try {
CredentialCipher::decrypt($this->key,
$pair['nonce'], $tampered);
$this->assertTrue(false,
"Tampered ciphertext should have thrown");
} catch (\Exception $e) {
$this->assertTrue(
str_contains($e->getMessage(), "authentication"),
"Exception explains the auth failure");
}
}
/**
* Decrypting with a wrong-length nonce should throw before
* sodium_crypto_secretbox_open gets a chance to.
*/
public function badNonceLengthTestCase()
{
$pair = CredentialCipher::encrypt($this->key, "secret");
$short_nonce = base64_encode("too short");
try {
CredentialCipher::decrypt($this->key,
$short_nonce, $pair['ciphertext']);
$this->assertTrue(false,
"Wrong-length nonce should have thrown");
} catch (\Exception $e) {
$this->assertTrue(
str_contains($e->getMessage(), "nonce"),
"Exception calls out the nonce length");
}
}
/**
* Garbage base64 in either input should throw rather than crash
* sodium_crypto_secretbox_open.
*/
public function malformedBase64TestCase()
{
try {
CredentialCipher::decrypt($this->key,
"***not base64***", "alsonot");
$this->assertTrue(false,
"Malformed base64 should have thrown");
} catch (\Exception $e) {
$this->assertTrue(
str_contains($e->getMessage(), "base64"),
"Exception calls out the bad base64");
}
}
}