There is no license information available for the latest version (v0.2.1) of this package.

v0.2.1 2020-07-22 17:51 UTC

This package is auto-updated.

Last update: 2024-10-23 03:19:58 UTC


README

A PHP implementation of Keybase's Saltpack encrypted/signed messaging format.

php-saltpack implements version 2.0 of Saltpack. All message types (encryption, attached signing, detached signing and signcryption) are supported.

Installation

php-saltpack is published to Packagist. GMP and Sodium are required.

composer require samuelthomas2774/saltpack

React is required for streaming.

composer require react/stream

Encryption

Encryption::encryptAndArmor encrypts a string and returns the ASCII-armored encrypted data.

Encryption::encrypt accepts the same arguments as Encryption::encryptAndArmor but returns a string without armor.

use Saltpack\Encryption;

$plaintext = '...';
$sender_keypair = sodium_crypto_box_keypair();
$recipients_keys = [
    sodium_crypto_box_publickey(sodium_crypto_box_keypair()),
];

$encrypted = Encryption::encryptAndArmor($plaintext, $sender_keypair, $recipients_keys);

// $encrypted === 'BEGIN SALTPACK ENCRYPTED MESSAGE. keDIDMQWYvVR58B FTfTeD305h3lDop TELGyPzBAAawRfZ rss3XwjQHK0irv7 rNIcmnvmn5YlTtK 7O1fFPePZGpx46P ...

php-saltpack also supports streaming encryption with Encryption::encryptAndArmorStream (or Encryption::encryptStream for encrypting without armor).

use Saltpack\Encryption;

$sender_keypair = sodium_crypto_box_keypair();
$recipients_keys = [
    sodium_crypto_box_publickey(sodium_crypto_box_keypair()),
];

$stream = Encryption::encryptAndArmorStream($sender_keypair, $recipients_keys);

// Write the encrypted and armored data to stdout
$stdout = new WritableResourceStream(STDOUT, $loop);
$stream->pipe($stdout);

$stream->end('...');

Messages can be decrypted with Encryption::dearmorAndDecrypt (or Encryption::decrypt if the message isn't armored).

use Saltpack\Encryption;

$encrypted = 'BEGIN SALTPACK ENCRYPTED MESSAGE. keDIDMQWYvVR58B FTfTeD305h3lDop TELGyPzBAAawRfZ rss3XwjQHK0irv7 rNIcmnvmn5YlTtK 7O1fFPePZGpx46P ...';
$recipient_keypair = sodium_crypto_box_keypair();

// If you know the sender's public key you can pass it to Encryption::dearmorAndDecrypt and it will throw if it doesn't match
$sender_key = sodium_crypto_box_publickey(sodium_crypto_box_keypair());

try {
    // $sender_key is passed by reference - if it is set this will throw if it doesn't match, otherwise it will be set to the sender's public key
    $decrypted = Encryption::dearmorAndDecrypt($encrypted, $recipient_keypair, $sender_key);

    // If you didn't pass the sender's public key you should check it now
    if ($sender_key !== hex2bin('...')) {
        throw new Exception('Sender public key doesn\'t match');
    }

    // $decrypted === '...'
} catch (\Saltpack\Exceptions\DecryptionError $err) {
    // Message could not be decrypted
} catch (\Saltpack\Exceptions\VerifyError $err) {
    // Message could not be verified
} catch (\Throwable $err) {
    //
}

Decryption also supports streaming with Encryption::dearmorAndDecryptStream or Encryption::decryptStream.

use Saltpack\Encryption;

$recipient_keypair = sodium_crypto_box_keypair();

// If you know the sender's public key you can pass it to Encryption::dearmorAndDecryptStream and it will emit an error if it doesn't match
$sender_key = sodium_crypto_box_publickey(sodium_crypto_box_keypair());

$stream = Encryption::dearmorAndDecryptStream($recipient_keypair, $sender_key);

$stream->on('end', () => {
    // If you didn't pass the sender's public key you should check it now
    if ($stream->sender_public_key !== hex2bin('...')) {
        throw new Exception('Sender public key doesn\'t match');
    }
});
$stream->on('error', function (\Exception $err) {
    //
});

// Write the encrypted and armored data to stdout
$stdout = new WritableResourceStream(STDOUT, $loop);
$stream->pipe($stdout);

$stream->end('BEGIN SALTPACK ENCRYPTED MESSAGE. keDIDMQWYvVR58B FTfTeD305h3lDop TELGyPzBAAawRfZ rss3XwjQHK0irv7 rNIcmnvmn5YlTtK 7O1fFPePZGpx46P ...');

Signing

Signing::signAndArmor signs a string and returns the ASCII-armored signed data.

Signing::sign accepts the same arguments as Signing::signAndArmor but returns a string without armor.

use Saltpack\Signing;

$plaintext = '...';
$signing_keypair = sodium_crypto_sign_keypair();

$signed = Signing::signAndArmor($plaintext, $signing_keypair);

// $signed === 'BEGIN SALTPACK SIGNED MESSAGE. kYM5h1pg6qz9UMn j6G9T0lmMjkYOsZ Kn4Acw58u39dn3B kmdpuvqpO3t2QdM CnBX5wO1ZIO8LTd knNlCR0WSEC0000 ...

Streaming is supported with Signing::signAndArmorStream or Signing::signStream.

use Saltpack\Signing;

$signing_keypair = sodium_crypto_sign_keypair();

$stream = Signing::signAndArmorStream($signing_keypair);

// Write the encrypted and armored data to stdout
$stdout = new WritableResourceStream(STDOUT, $loop);
$stream->pipe($stdout);

$stream->end('...');

Signed messages can be verified and read with Signing::dearmorAndVerify or Signing::verify.

use Saltpack\Signing;

$signed = 'BEGIN SALTPACK SIGNED MESSAGE. kYM5h1pg6qz9UMn j6G9T0lmMjkYOsZ Kn4Acw58u39dn3B kmdpuvqpO3t2QdM CnBX5wO1ZIO8LTd knNlCR0WSEC0000 ...';

// If you know the sender's public key you can pass it to Signing::dearmorAndVerify and it will throw if it doesn't match
$sender_key = sodium_crypto_sign_publickey(sodium_crypto_sign_keypair());

try {
    // $sender_key is passed by reference - if it is set this will throw if it doesn't match, otherwise it will be set to the sender's public key
    $verified = Signing::dearmorAndVerify($signed, $sender_key);

    // If you didn't pass the sender's public key you should check it now
    if ($sender_key !== hex2bin('...')) {
        throw new Exception('Sender public key doesn\'t match');
    }

    // $verified === '...'
} catch (\Saltpack\Exceptions\VerifyError $err) {
    // Message could not be verified
} catch (\Throwable $err) {
    //
}

Reading signed messages also supports streaming with Signing::dearmorAndVerifyStream or Signing::verifyStream.

use Saltpack\Signing;

// If you know the sender's public key you can pass it to Signing::dearmorAndVerifyStream and it will throw if it doesn't match
$sender_key = sodium_crypto_sign_publickey(sodium_crypto_sign_keypair());

$stream = Signing::dearmorAndVerifyStream($sender_key);

$stream->on('end', () => {
    // If you didn't pass the sender's public key you should check it now
    if ($stream->public_key !== hex2bin('...')) {
        throw new Exception('Sender public key doesn\'t match');
    }
});
$stream->on('error', function (\Exception $err) {
    //
});

// Write the verified data to stdout
$stdout = new WritableResourceStream(STDOUT, $loop);
$stream->pipe($stdout);

$stream->end('BEGIN SALTPACK SIGNED MESSAGE. kYM5h1pg6qz9UMn j6G9T0lmMjkYOsZ Kn4Acw58u39dn3B kmdpuvqpO3t2QdM CnBX5wO1ZIO8LTd knNlCR0WSEC0000 ...');

Detached signing

Signing::signDetachedAndArmor signs a string and returns the ASCII-armored signature.

Signing::signDetached accepts the same arguments as Signing::signDetachedAndArmor but returns a string without armor.

Detached signing/verifying does not support streaming yet.

use Saltpack\Signing;

$plaintext = '...';
$signing_keypair = sodium_crypto_sign_keypair();

$signed = Signing::signDetachedAndArmor($plaintext, $signing_keypair);

// $signed === 'BEGIN SALTPACK DETACHED SIGNATURE. kYM5h1pg6qz9UMn j6G9T0tZQlxoky3 0YoKQ4s21IrFv3B kmdpuvqpO3t2QdM CnBX5wO1ZIO8LTd knNlCR0WSEC0000 ...

Detached signatures can be verified with Signing::dearmorAndVerifyDetached or Signing::verifyDetached.

use Saltpack\Signing;

$signed = 'BEGIN SALTPACK SIGNED MESSAGE. kYM5h1pg6qz9UMn j6G9T0lmMjkYOsZ Kn4Acw58u39dn3B kmdpuvqpO3t2QdM CnBX5wO1ZIO8LTd knNlCR0WSEC0000 ...';
$plaintext = '...';

// If you know the sender's public key you can pass it to dearmorAndVerifyDetached and it will throw if it doesn't match
$sender_key = sodium_crypto_sign_publickey(sodium_crypto_sign_keypair());

try {
    // $sender_key is passed by reference - if it is set this will throw if it doesn't match, otherwise it will be set to the sender's public key
    Signing::dearmorAndVerifyDetached($signature, $plaintext, $sender_key);

    // If you didn't pass the sender's public key you should check it now
    if ($sender_key !== hex2bin('...')) {
        throw new Exception('Sender public key doesn\'t match');
    }
} catch (\Saltpack\Exceptions\VerifyError $err) {
    // Message could not be verified
} catch (\Throwable $err) {
    //
}

Signcryption

Signcryption is very similar to Saltpack's usual encryption format, but:

  • The sender uses an Ed25519 signing key instead of an X25519 encryption key,
  • A symmetric key can be provided for a group of recipients instead of each recipient having their own encryption key (this is not implemented by php-saltpack yet, though the internal APIs are there), and
  • Messages are not repudiable, which means anyone who has a copy of the message and a decryption key can verify it's authenticity, not just intended recipients.

Signcryption::encryptAndArmor encrypts a string and returns the ASCII-armored signcrypted data.

Signcryption::encrypt accepts the same arguments as Signcryption::encryptAndArmor but returns a string without armor.

use Saltpack\Signcryption;

$plaintext = '...';
$sender_keypair = sodium_crypto_sign_keypair();
$recipients_keys = [
    // TODO: how can a recipient identifier and symmetric key be provided?
    sodium_crypto_box_publickey(sodium_crypto_box_keypair()),
];

$signcrypted = Signcryption::encryptAndArmor($plaintext, $sender_keypair, $recipients_keys);

// $signcrypted === 'BEGIN SALTPACK ENCRYPTED MESSAGE. keDIDMQWYvVR58B FTfTeDQNHnhYI5G UXZkLqLqVvhmpfZ rss3XwjQHK0irv7 rNIcmnvmn5RTzTR OPZLLRr1s0DEZtS ...

Streaming is supported with Signcryption::encryptAndArmorStream or (Signcryption::encryptStream for encrypting without armor).

use Saltpack\Signcryption;

$sender_keypair = sodium_crypto_sign_keypair();
$recipients_keys = [
    // TODO: how can a recipient identifier and symmetric key be provided?
    sodium_crypto_box_publickey(sodium_crypto_box_keypair()),
];

$stream = Signcryption::encryptAndArmorStream($sender_keypair, $recipients_keys);

// Write the verified data to stdout
$stdout = new WritableResourceStream(STDOUT, $loop);
$stream->pipe($stdout);

$stream->end('...');

Messages can be decrypted with Signcryption::dearmorAndDecrypt (or Signcryption::decrypt if the message isn't armored).

use Saltpack\Signcryption;

$encrypted = 'BEGIN SALTPACK ENCRYPTED MESSAGE. keDIDMQWYvVR58B FTfTeDQNHnhYI5G UXZkLqLqVvhmpfZ rss3XwjQHK0irv7 rNIcmnvmn5RTzTR OPZLLRr1s0DEZtS ...';
// TODO: how can a recipient identifier and symmetric key be provided?
// How can multiple keys be provided (as a recipient may have multiple shared symmetric keys that may be used for this message)
$recipient_keypair = sodium_crypto_box_keypair();

// If you know the sender's public key you can pass it to Signcryption::dearmorAndDecrypt and it will throw if it doesn't match
$sender_key = sodium_crypto_sign_publickey(sodium_crypto_sign_keypair());

try {
    // $sender_key is passed by reference - if it is set this will throw if it doesn't match, otherwise it will be set to the sender's public key
    $decrypted = Signcryption::dearmorAndDecrypt($encrypted, $recipient_keypair, $sender_key);

    // If you didn't pass the sender's public key you should check it now
    if ($sender_key !== hex2bin('...')) {
        throw new Exception('Sender public key doesn\'t match');
    }

    // $decrypted === '...'
} catch (\Saltpack\Exceptions\DecryptionError $err) {
    // Message could not be decrypted
} catch (\Saltpack\Exceptions\VerifyError $err) {
    // Message could not be verified
} catch (\Throwable $err) {
    //
}

Decryption also supports streaming with Signcryption::dearmorAndDecryptStream or Signcryption::decryptStream.

use Saltpack\Signcryption;

// TODO: how can a recipient identifier and symmetric key be provided?
// How can multiple keys be provided (as a recipient may have multiple shared symmetric keys that may be used for this message)
$recipient_keypair = sodium_crypto_box_keypair();

// If you know the sender's public key you can pass it to Signcryption::dearmorAndDecryptStream and it will emit an error if it doesn't match
$sender_key = sodium_crypto_sign_publickey(sodium_crypto_sign_keypair());

$stream = Signcryption::dearmorAndDecryptStream($recipient_keypair, $sender_key);

$stream->on('end', () => {
    // If you didn't pass the sender's public key you should check it now
    if ($stream->sender_public_key !== hex2bin('...')) {
        throw new Exception('Sender public key doesn\'t match');
    }
});
$stream->on('error', function (\Exception $err) {
    //
});

// Write the encrypted and armored data to stdout
$stdout = new WritableResourceStream(STDOUT, $loop);
$stream->pipe($stdout);

$stream->end('BEGIN SALTPACK ENCRYPTED MESSAGE. keDIDMQWYvVR58B FTfTeDQNHnhYI5G UXZkLqLqVvhmpfZ rss3XwjQHK0irv7 rNIcmnvmn5RTzTR OPZLLRr1s0DEZtS ...');

Additional notes

  • php-saltpack always chunks input data to 1 MB payloads.
  • php-saltpack is fully tested with node-saltpack.
  • php-saltpack is partially tested with Keybase:
    • Encrypted messages created by node-saltpack and php-saltpack can be decrypted with Keybase.
    • Signcrypted messages created by node-saltpack and php-saltpack can be decrypted with Keybase.
    • Signed messages created by Keybase can be verified with node-saltpack and php-saltpack.
    • Signed messages created by node-saltpack and php-saltpack can be read by Keybase.

License

php-saltpack is released under the MIT license. Saltpack is designed by the Keybase developers, and uses NaCl for crypto and MessagePack for binary encoding. node-saltpack and php-saltpack's armoring implementation is based on saltpack-ruby.