Last active
July 21, 2022 13:27
-
-
Save patoui/836cd1ad9d1f4a38248b114b098cf12e to your computer and use it in GitHub Desktop.
Temporary/short lived secrets script
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Example of output, empty output (usually) means either the password is incorrect or the secret has expired
This would be better suited for a web app:
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 :)