Skip to content

Instantly share code, notes, and snippets.

@patoui
Last active July 21, 2022 13:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save patoui/836cd1ad9d1f4a38248b114b098cf12e to your computer and use it in GitHub Desktop.
Save patoui/836cd1ad9d1f4a38248b114b098cf12e to your computer and use it in GitHub Desktop.
Temporary/short lived secrets script
<?php
declare(strict_types=1);
/**
* Tested with PHP 8.1
* Create a sqlite file named `secrets.db` in the same directory
* Required extensions: sqlite3/pdo_sqlite, openssl
* Recommended extension: uuid
*
*
* Storing a secret:
*
* // requires password of 'foobar' to retrieve the content
* php index.php --content="Big secret" --password=foobar
*
* // requires password of 'foobar' to retrieve the content and expires in 5 seconds
* php index.php --content="Big secret" --password=foobar --expiry=5
*
*
* Retrieving a secret:
*
* php index.php --id=fc74c2f1-103d-4e65-9a60-bbadc364d1f6 --password=password
*/
const CIPHER = 'aes-256-cbc';
function conn(): PDO {
static $pdo;
if ($pdo) {
return $pdo;
}
$pdo = new PDO('sqlite:secrets.db');
$create_table = <<<SQL
CREATE TABLE IF NOT EXISTS secrets (
`id` TEXT UNIQUE,
`content` TEXT,
`password` TEXT,
`timestamp` INTEGER
);
SQL;
if (!$pdo->prepare($create_table)->execute()) {
throw new \RuntimeException('Unable to create secrets table');
}
// remove any expired secrets
conn()->prepare('DELETE FROM secrets WHERE `timestamp` < ?')->execute([time()]);
return $pdo;
}
/**
* Attempt to retrieve the secret message
* @param string $id id to reference the message by
* @param string $password password to gain access to the secret message
* @return string the secret message or empty string if the password is invalid
* @throws PDOException
* @throws RuntimeException
*/
function retrieve(string $id, string $password): string {
$stmt = conn()->prepare('SELECT content, `password`, `timestamp` FROM secrets WHERE id = ?');
if (!$stmt->execute([$id])) {
// executing prepared statement failed
return '';
}
$data = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$data) {
// no results returned
return '';
}
// secret expired
if (((int) $data['timestamp']) < time()) {
return '';
}
if (!password_verify($password, $data['password'])) {
// invalid password
return '';
}
return decrypt($password, $data['content']);
}
/**
* Encrypt the message
* @param string $key the key of which to encrypt with
* @param string $content the content to encrypt
* @return string the encrypted content
*/
function encrypt(string $key, string $content): string
{
$ivlen = openssl_cipher_iv_length(CIPHER);
$iv = openssl_random_pseudo_bytes($ivlen);
$ciphertext = openssl_encrypt($content, CIPHER, $key, 0, $iv);
return base64_encode($iv.$ciphertext);
}
/**
* Attempt to decrypt the message
* @param string $key the key of which to encrypt with
* @param string $encrypted_message the encrypted message
* @return string the decrypted message
*/
function decrypt(string $key, string $encrypted_message): string
{
$encrypted_message = base64_decode($encrypted_message);
if (!$encrypted_message) {
return '';
}
// get the length of the initialization vector
$ivlen = openssl_cipher_iv_length(CIPHER);
// extract initialization vector from encrypted message
$iv = substr($encrypted_message, 0, $ivlen);
// remove initialization vector from encrypted message
$encrypted_message = substr($encrypted_message, $ivlen);
if ($decrypted_message = openssl_decrypt($encrypted_message, CIPHER, $key, 0, $iv)) {
return $decrypted_message;
}
return '';
}
/**
* Store the secret message
* @param string $content the secret message content
* @param string $password the password for which to gain access to the message
* @param int|null $expiry optional expiry, defaults to 30 seconds
* @return string the ID of the stored secret message to share with others
* @throws PDOException
* @throws RuntimeException
*/
function store(
string $content,
string $password,
int $expiry = null
): string {
$was_successful = conn()->prepare('INSERT INTO secrets (id, content, `password`, `timestamp`) VALUES (?,?,?,?);')
->execute([
$id = function_exists('uuid_create') ? uuid_create() : str_replace('.', '', uniqid('', true)),
encrypt($password, $content),
password_hash($password, PASSWORD_BCRYPT),
time() + ($expiry ?? 30)
]);
if (!$was_successful) {
return '';
}
return $id;
}
/**
* Handle the input to either store a secret or retrieve one
* @param array $data 4 possible keys:
* - id: used to retrieve a secret
* - content: the content of the secret message
* - password: used to retrieve a secret when paired with 'id' or used to
* define what password to use for a new secret when paired with
* 'content' key
* - expiry (optional): optional parameter to define a custom expiry for the
* secret message, defaults to 30 seconds
* @return string either the secret message when used with 'id' or the content
* of the id of the secret when used with 'content'
* @throws InvalidArgumentException
* @throws PDOException
* @throws RuntimeException
*/
function handle(array $data): string {
if (empty($data['password'])) {
throw new \InvalidArgumentException("The 'password' field is required.");
}
if (!empty($data['id'])) {
return retrieve($data['id'], $data['password']);
}
if (!empty($data['content'])) {
return store(
$data['content'],
$data['password'],
!empty($data['expiry']) && is_numeric($data['expiry']) ? (int) $data['expiry'] : null
);
}
throw new \InvalidArgumentException("Either 'id' or 'content' is required.");
}
echo handle(
getopt('', ['content::', 'id::', 'password::', 'expiry::'])
) . PHP_EOL;
@patoui
Copy link
Author

patoui commented Jul 20, 2022

Example of output, empty output (usually) means either the password is incorrect or the secret has expired
image

This would be better suited for a web app:

  • 1 page to create secrets
  • 1 page to retrieve secrets (with optional password prompt)

To further security and limit secret access, the content could be encrypted with the password. Maybe I'll come back to this and add that in :)

Just felt like messing around with this idea, this is a first rough attempt :)

@patoui
Copy link
Author

patoui commented Jul 20, 2022

Went ahead and added encryption to the content of the secret, also now requires a password

Example usage and SQLite query to show it's encrypted in the database table
image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment