<?php

/**
 * @author    Jim Wigginton <terrafrost@php.net>
 * @copyright 2025-2026 Jim Wigginton
 * @license   http://www.opensource.org/licenses/mit-license.html  MIT License
 */

declare(strict_types=1);

namespace phpseclib4\Tests\Unit\File\X509;

use phpseclib4\Crypt\RSA\PrivateKey as RSAPrivateKey;
use phpseclib4\Crypt\EC;
use phpseclib4\Crypt\EC\PrivateKey as ECPrivateKey;
use phpseclib4\File\PFX;
use phpseclib4\File\X509;
use phpseclib4\Tests\PhpseclibTestCase;

class PFXTest extends PhpseclibTestCase
{
    public function testEncryptedComplicated(): void
    {
        // pfx1.der contains 2x key pairs (ie. 2x X509 certs and 2x private keys)
        // and 1x "SecretBag" (an AES secret key), which phpseclib doesn't support
        // the PFX was generated with https://keystore-explorer.org/
        $temp = file_get_contents(__DIR__ . '/pfx1.der');
        $r = PFX::load($temp, 'password');
        $r->removePassword();

        $linearized = $r->getAll();
        $this->assertCount(4, $linearized);
        $this->assertInstanceOf(RSAPrivateKey::class, $linearized[0]);
        $this->assertInstanceOf(ECPrivateKey::class, $linearized[1]);
        $this->assertInstanceOf(X509::class, $linearized[2]);
        $this->assertInstanceOf(X509::class, $linearized[3]);

        $privateKeys = $r->getPrivateKeys();
        $this->assertCount(2, $privateKeys);

        $this->assertIsArray($r->toArray());
        $this->assertIsArray($r->keys());

        $friendly = $r->getFriendlyNames();
        $this->assertCount(3, $friendly);
        // each of the friendly names are instances of BMPString, which encodes every character with two bytes
        // if the second byte isn't needed it's the null byte
        $this->assertSame('a', (string) $friendly[0]->toUTF8String());
        $this->assertSame('b', (string) $friendly[1]->toUTF8String());
        $this->assertSame('c', (string) $friendly[2]->toUTF8String());

        $local = $r->getLocalKeyIDs();
        $this->assertCount(3, $local);
        $this->assertSame('54696d652031373439393330383037373833', bin2hex((string) $local[0]));
        $this->assertSame('54696d652031373439393330383234303239', bin2hex((string) $local[1]));
        $this->assertSame('54696d652031373439393330383338363932', bin2hex((string) $local[2]));

        $a = $r->pluckByFriendlyName('a');
        $this->assertCount(2, $a);
        $this->assertInstanceOf(RSAPrivateKey::class, $a[0]);
        $this->assertInstanceOf(X509::class, $a[1]);
        // two objects with the same friendly name don't necessarily *have* to correspond
        // to one another but, in this case, they do
        $t1 = (string) $a[0]->getPublicKey();
        $t2 = (string) $a[1]->getPublicKey();
        $this->assertSame($t1, $t2);

        $b = $r->pluckByFriendlyName('b');
        $this->assertCount(2, $b);
        $this->assertInstanceOf(ECPrivateKey::class, $b[0]);
        $this->assertInstanceOf(X509::class, $b[1]);
        // two objects with the same friendly name don't necessarily *have* to correspond
        // to one another but, in this case, they do
        $t1 = (string) $b[0]->getPublicKey();
        $t2 = (string) $b[1]->getPublicKey();
        $this->assertSame($t1, $t2);

        // c exists as a friendly name - it just doesn't have any objects that phpseclib can deal with
        $c = $r->pluckByFriendlyName('c');
        $this->assertCount(0, $c);

        // d doesn't exist as a friendly name
        $d = $r->pluckByFriendlyName('d');
        $this->assertCount(0, $d);

        $a = $r->pluckByLocalKeyID(hex2bin('54696d652031373439393330383037373833'));
        $this->assertCount(2, $a);
        $this->assertInstanceOf(RSAPrivateKey::class, $a[0]);
        $this->assertInstanceOf(X509::class, $a[1]);
        $t1 = (string) $a[0]->getPublicKey();
        $t2 = (string) $a[1]->getPublicKey();
        $this->assertSame($t1, $t2);

        $b = $r->pluckByLocalKeyID(hex2bin('54696d652031373439393330383234303239'));
        $this->assertCount(2, $b);
        $this->assertInstanceOf(ECPrivateKey::class, $b[0]);
        $this->assertInstanceOf(X509::class, $b[1]);
        // two objects with the same friendly name don't necessarily *have* to correspond
        // to one another but, in this case, they do
        $t1 = (string) $b[0]->getPublicKey();
        $t2 = (string) $b[1]->getPublicKey();
        $this->assertSame($t1, $t2);

        $c = $r->pluckByLocalKeyID(hex2bin('54696d652031373439393330383338363932'));
        $this->assertCount(0, $c);

        $new = $r->pfxFromFriendlyName('a');
        $linearized = $new->getAll();
        $this->assertCount(2, $linearized);
        $ref = ' ';
        // hex2bin($ref) successfully decodes with the following:
        // openssl pkcs12 -info -in test.der -nodes
        $this->assertSame($ref, bin2hex("$new"));
    }

    public function testUnencryptedSimple(): void
    {
        $temp = file_get_contents(__DIR__ . '/pfx2.der');
        $pfx = PFX::load($temp);
        $old = "$pfx";
        $this->assertIsString($old);
        $pfx->setPassword('password');
        $new = "$pfx";
        $this->assertIsString($new);
        $this->assertNotEquals($old, $new);

        $x509 = new X509(EC::createKey('nistp256')->getPublicKey());
        $old = "$x509";
        $result = $pfx->sign($x509);
        $this->assertIsString($result);
        $new = "$x509";
        $this->assertIsString($new);
        $this->assertNotEquals($old, $new);
    }

    public function testEncryptedNoMAC(): void
    {
        $temp = file_get_contents(__DIR__ . '/pfx3.der');
        $pfx = PFX::load($temp, 'password');
        $old = "$pfx";
        $this->assertIsString($old);
    }

    public function testUnencryptedWithMAC(): void
    {
        $temp = file_get_contents(__DIR__ . '/pfx4.der');
        $pfx = PFX::load($temp);
        $old = "$pfx";
        $this->assertIsString($old);
    }

    public function testMSExport(): void
    {
        $temp = file_get_contents(__DIR__ . '/pfx5.der');
        $pfx = PFX::load($temp, 'password');
        $old = "$pfx";
        $this->assertIsString($old);
    }

    public function testCreatePFX(): void
    {
        $key = EC::createKey('Ed25519');
        $x509 = new X509($key->getPublicKey());
        $x509->setDN('O=phpseclib');
        $x509->makeCA();
        $key->sign($x509);
        $pfx = new PFX();
        $pfx->add($key);
        $pfx->add($x509);

        $key = EC::createKey('nistp256');
        $x509 = new X509($key->getPublicKey());
        $pfx->sign($x509);

        $pfx->removePassword();

        $str = "$pfx";
        $this->assertIsString($str);
        // these will almost always be different for encrypted PFXs because, among other things, the salt
        // for the MAC, is generated using random_bytes()
        $this->assertSame($pfx->getEncoded(), $str);

        X509::addCA((string) $pfx->getCertificates()[0]);
        $this->assertTrue($x509->validateSignature());
    }

    public function testCreateEncryptedFromScratch(): void
    {
        $private = EC::createKey('secp256k1');
        $x509 = new X509($private->getPublicKey());
        $x509->setDN('CN=test');
        $x509->makeCA();
        $private->sign($x509);

        $pfx = new PFX();
        $pfx->removePassword();
        $pfx->add($private);
        $pfx->add($x509);
        $pfx->setPassword('password');
        $str = "$pfx";
        $this->assertIsString($str);
    }

    public function testCreateEncryptedPasswordChange(): void
    {
        $private = EC::createKey('secp256k1');
        $x509 = new X509($private->getPublicKey());
        $x509->setDN('CN=test');
        $x509->makeCA();
        $private->sign($x509);

        $pfx = new PFX();
        $pfx->add($private);
        $pfx->add($x509);
        $pfx->setPassword('password');
        $str = "$pfx";
        $this->assertIsString($str);
    }

    public function testCreateEncryptedAltPFX(): void
    {
        $private = EC::createKey('secp256k1');
        $x509 = new X509($private->getPublicKey());
        $x509->setDN('CN=test2');
        $x509->makeCA();
        $private->sign($x509);

        $pfx = new PFX();
        $pfx->removePassword();
        $pfx->add($private);
        $pfx['authSafe']['content'][0]['content'][] = [
            'bagId' => 'CertBag',
            'bagValue' => [
                'certId' => 'x509Certificate',
                'certValue' => $x509,
            ]
        ];
        /*
        what $pfx->add($x509) does is this:

        $pfx['authSafe']['content'][] = ...;

        vs:

        $pfx['authSafe']['content'][0]['content'][] = ...;
        */
        $pfx->setPassword('password');
        $str = "$pfx";
        $this->assertIsString($str);
    }

    public function testCreateLoadPFX(): void
    {
        $pfx = new PFX();
        $pfx->setPassword('');

        $private = EC::createKey('nistp256');

        $x509 = new X509($private->getPublicKey());
        $x509->setDN('CN=test');
        $x509->makeCA();
        $private->sign($x509);

        $pfx->add($private);
        $pfx->add($x509);

        $pfx = PFX::load("$pfx", '')->toArray();
        $this->assertIsArray($pfx);
    }
}
