Add alternate idea/implementation

This commit is contained in:
moparisthebest 2014-08-23 19:14:14 -04:00
parent cdbefb1268
commit 02787044b6
4 changed files with 334 additions and 0 deletions

22
alternate/readme.md Normal file
View File

@ -0,0 +1,22 @@
#### Here is the idea:
1. This script stores and returns binary blobs, so really, anything.
2. Every blob has extra attributes which can be set by additional parameters on creation, these are:
1. id (required), uniquely identifies each blob when combined with key
2. key (required), used to encrypt/decrypt blob on storage/access
3. file, the blob to encrypt and store, ideally this is already encrypted with a local key that never leaves your computer before sent to this script.
4. time-to-live (HOURS where 1 => time-to-live <= 24), if it hasn't been successfully accessed within X hours, all traces of it will be securely deleted (by a cronjob, not in PHP)
5. tmp (true/false), stores the blob in in-memory storage, with the hope that if the machine is powered off everything disappears
3. Sending in only an id and key will decrypt the blob and send it back to the browser, if nothing exists for that id/key, a new blob will be created from $new_blob_source with sent in parameters or defaults, stored, and sent back.
4. Sending in an id, key, and file will save (and overwrite if id/key was set before) the file to be served back when requested again, with optionally overridden defaults based on the other parameters sent in.
5. Every time a blob is successfully accessed (correct id and key), the time will be saved. This will be used by the secure deleting cronjob.
I am looking for feedback on how *secure* this idea is, if there are flaws in the approach or potential weaknesses I don't see, and ways to improve it.
Any improvements that can be made in the reference implementations will be appreciated as well.
#### In this repo
1. secureblob.php - Reference implementation in PHP
2. secureblob_cron.sh - Reference implementation of cleaning cronjob
3. secureblob_up.sh - Upload script to test reference implementations
4. agpl-3.0.txt - License all code is released under

258
alternate/secureblob.php Normal file
View File

@ -0,0 +1,258 @@
<?php
/*
secureblob.php https://github.com/moparisthebest/secureblob
Copyright (C) 2014 moparisthebest (Travis Burtrum)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
global $blob_path, $tmp_blob_path, $new_blob_source, $default_content_type, $max_size, $min_size;
$blob_path = '/tmp/secureblob/';
$tmp_blob_path = '/run/shm/secureblob/';
$new_blob_source = '/dev/urandom';
//$new_blob_source = '/dev/zero';
//$new_blob_source = '/run/shm/bob';
$default_content_type = 'application/octet-stream';
//$max_size = 4 * 1024 * 1024; // 4mb
$max_size = 64 * 1024; // 64kb
$min_size = 16;
// params for my encrypt/decrypt/stretch_key functions, if you write your own functions, ignore these params
global $mcrypt_cipher, $mcrypt_mode, $mcrypt_rand_src, $pbkdf2_hash, $pbkdf2_iterations, $salt_length;
$mcrypt_cipher = MCRYPT_RIJNDAEL_128;
//$mcrypt_cipher = MCRYPT_RIJNDAEL_256;
//$mcrypt_cipher = MCRYPT_BLOWFISH;
$mcrypt_mode = MCRYPT_MODE_CBC;
$mcrypt_rand_src = MCRYPT_DEV_RANDOM;
$pbkdf2_hash = 'sha512';
$pbkdf2_iterations = 65536;
$salt_length = 32;
// todo: include config file
function encrypt($decrypted, $password) {
global $mcrypt_cipher, $mcrypt_mode, $mcrypt_rand_src, $salt_length;
//echo "decrypted: '$decrypted'<br/>\n";
srand();
// first generate a random $salt_length character salt
$salt_chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"\'~!@#$%^&*(){}[],./?';
$salt_chars_len = strlen($salt_chars);
$salt = '';
for ($i = 0; $i < $salt_length; $i++)
$salt .= $salt_chars[rand(0, $salt_chars_len)];
//echo "salt: '$salt'<br/>\n";
// stretch the key
$key = stretch_key($password, $salt, mcrypt_get_key_size($mcrypt_cipher, $mcrypt_mode));
// Build $iv and $iv_base64. We use a block size of 128 bits (AES compliant) and CBC mode. (Note: ECB mode is inadequate as IV is not used.)
$iv = mcrypt_create_iv(mcrypt_get_iv_size($mcrypt_cipher, $mcrypt_mode), $mcrypt_rand_src);
$iv_base64 = base64_encode($iv);
//echo "iv_base64: '$iv_base64'<br/>\n";
// Encrypt $decrypted plus a random character to be removed so we know the last character isn't \0
$encrypted = base64_encode(mcrypt_encrypt($mcrypt_cipher, $key, $decrypted . $salt_chars[rand(0, $salt_chars_len)], $mcrypt_mode, $iv));
//echo "encrypted: '$encrypted'<br/>\n";
// We're done!
return "$salt:$mcrypt_cipher:$mcrypt_mode:$iv_base64:$encrypted";
}
function decrypt($encrypted, $password, $check_integrity = true) {
$encrypted = explode(':', $encrypted); // salt, mcrypt_cipher, mcrypt_mode, iv_base64, encrypted_base64
// retrieve $salt from $encrypted
$salt = $encrypted[0];
$mcrypt_cipher = $encrypted[1];
$mcrypt_mode = $encrypted[2];
// stretch the key
$key = stretch_key($password, $salt, mcrypt_get_key_size($mcrypt_cipher, $mcrypt_mode));
// Retrieve $iv which is base64_decoded.
$iv = base64_decode($encrypted[3]);
// Decrypt the data. rtrim won't corrupt the data because the last 32 characters are the md5 hash; thus any \0 character has to be padding.
$decrypted = rtrim(mcrypt_decrypt($mcrypt_cipher, $key, base64_decode($encrypted[4]), $mcrypt_mode, $iv), "\0\4");
// Remove the last character from $decrypted.
$decrypted = substr($decrypted, 0, -1);
// Yay!
return $decrypted;
}
function stretch_key($key, $salt, $max_key_size){
global $pbkdf2_hash, $pbkdf2_iterations;
//echo "max_key_size: '$max_key_size'<br/>\n";
//return hash('SHA256', $salt . $key, true);
return pbkdf2($pbkdf2_hash, $key, $salt, $pbkdf2_iterations, $max_key_size, true);
}
function secure_delete_file($filename){
$size = filesize($filename);
$src = fopen('/dev/zero', 'rb');
$dest = fopen($filename, 'wb');
stream_copy_to_stream($src, $dest, $size);
fclose($src);
fclose($dest);
unlink($filename);
}
function secure_delete_folder($path) {
$it = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($path),
RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($it as $file) {
if (in_array($file->getBasename(), array('.', '..'))) {
continue;
} elseif ($file->isDir()) {
rmdir($file->getPathname());
} elseif ($file->isFile() || $file->isLink()) {
secure_delete_file($file->getPathname());
}
}
rmdir($path);
}
function write_to_file($fname, $contents, $mode = 'w') {
$fh = fopen($fname, $mode);
fwrite($fh, $contents);
fclose($fh);
}
function encrypt_write($folder, $key, $decrypted = NULL){
if($decrypted === NULL)
$decrypted = create_blob();
if(!file_exists($folder)) {
// create directory
mkdir($folder, 0700, true);
// write attributes
$time_to_live = setDefaultLimits('time-to-live', 1, 1, 24);
write_to_file($folder.'time-to-live', $time_to_live);
}
write_to_file($folder.'blob', encrypt($decrypted, $key), 'wb');
return $decrypted;
}
// creates a blob from $new_blob_source
function create_blob(){
global $new_blob_source, $max_size, $min_size;
srand();
return file_get_contents($new_blob_source, false, NULL, 0, rand( (($max_size-$min_size)/2), $max_size)); // size is some random amount between halfway between max_size and min_size and max_size
}
/*
* PBKDF2 key derivation function as defined by RSA's PKCS #5: https://www.ietf.org/rfc/rfc2898.txt
* $algorithm - The hash algorithm to use. Recommended: SHA256
* $password - The password.
* $salt - A salt that is unique to the password.
* $count - Iteration count. Higher is better, but slower. Recommended: At least 1000.
* $key_length - The length of the derived key in bytes.
* $raw_output - If true, the key is returned in raw binary format. Hex encoded otherwise.
* Returns: A $key_length-byte key derived from the password and salt.
*
* Test vectors can be found here: https://www.ietf.org/rfc/rfc6070.txt
*
* This implementation of PBKDF2 was originally created by https://defuse.ca
* With improvements by http://www.variations-of-shadow.com
*/
function pbkdf2($algorithm, $password, $salt, $count, $key_length, $raw_output = false)
{
$algorithm = strtolower($algorithm);
if(!in_array($algorithm, hash_algos(), true))
trigger_error('PBKDF2 ERROR: Invalid hash algorithm.', E_USER_ERROR);
if($count <= 0 || $key_length <= 0)
trigger_error('PBKDF2 ERROR: Invalid parameters.', E_USER_ERROR);
if (function_exists("hash_pbkdf2")) {
// The output length is in NIBBLES (4-bits) if $raw_output is false!
if (!$raw_output) {
$key_length = $key_length * 2;
}
return hash_pbkdf2($algorithm, $password, $salt, $count, $key_length, $raw_output);
}
$hash_length = strlen(hash($algorithm, "", true));
$block_count = ceil($key_length / $hash_length);
$output = "";
for($i = 1; $i <= $block_count; $i++) {
// $i encoded as 4 bytes, big endian.
$last = $salt . pack("N", $i);
// first iteration
$last = $xorsum = hash_hmac($algorithm, $last, $password, true);
// perform the other $count - 1 iterations
for ($j = 1; $j < $count; $j++) {
$xorsum ^= ($last = hash_hmac($algorithm, $last, $password, true));
}
$output .= $xorsum;
}
if($raw_output)
return substr($output, 0, $key_length);
else
return bin2hex(substr($output, 0, $key_length));
}
function setRequired($name){
if(!isset($_REQUEST[$name]))
die("All parameters must be set."); // intentionally vague
return $_REQUEST[$name];
}
function setDefault($name, $default){
return isset($_REQUEST[$name]) ? $_REQUEST[$name] : $default;
}
// this function does no bounds checking on lower/upper, so don't send in user input
function setDefaultLimits($name, $default, $lower, $upper){
$ret = setDefault($name, $default);
if($ret < $lower)
return $lower;
if($ret > $upper)
return $upper;
return $ret;
}
$id = setRequired('id');
$key = setRequired('key');
$tmp = setDefault('tmp', true) !== 'false'; // default is true
$folder = ($tmp ? $tmp_blob_path : $blob_path).hash('sha512', $id . stretch_key($key, 'hau98grch348rcueoahic34hce.i', 128)).'/';
// I'd like the file to never get to the filesystem at all, any good solutions for this?
// http://stackoverflow.com/questions/5701508/storing-php-php-fpm-apaches-temporary-from-upload-files-in-ram-rather-than-th
$file = $_FILES['file'];
$decrypted = '';
if(isset($file)){
// then we want to SET a new file that was sent in
if($file['size'] > $max_size)
die('Max file size exceeded');
if($file['size'] < $min_size)
die('Min file size not reached');
$decrypted = file_get_contents($file['tmp_name']);
// delete unencrypted file and previous folder if it exists
secure_delete_file($file['tmp_name']);
secure_delete_folder($folder);
encrypt_write($folder, $key, $decrypted);
//echo "serving new real just sent in<br />\n";
} else if(!file_exists($folder)) {
// then we want to SET a new file we create from $new_blob_source
$decrypted = encrypt_write($folder, $key);
//echo "serving new real just generated<br />\n";
} else {
// otherwise we want to serve an existing file
$fname = $folder.'blob';
$decrypted = decrypt(file_get_contents($fname), $key);
touch($fname);
}
header('Content-Type: '.setDefault('content-type', $default_content_type));
echo $decrypted;
//echo "decrypted: '$decrypted'<br/>\n";
?>

45
alternate/secureblob_cron.sh Executable file
View File

@ -0,0 +1,45 @@
#!/bin/bash
# secureblob.php https://github.com/moparisthebest/secureblob
# Copyright (C) 2014 moparisthebest (Travis Burtrum)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
file_age_hours(){
file="$1"
from_date="$2"
[ -z "$from_date" ] && from_date="$(date +%s)"
seconds_diff="$(($from_date - $(stat -c '%Y' "$file")))"
echo "$(($seconds_diff/60/60))"
}
from_date="$(date +%s)"
find /tmp/secureblob /run/shm/secureblob -type f -name blob -mmin +60 | while read file
do
dir="$(dirname "$file")"
[ "$(file_age_hours "$file" "$from_date")" -lt "$(cat "$dir/time-to-live")" ] 2>/dev/null || {
# done this way so if time-to-live isn't a proper number we delete everything
find "$dir" -type f -exec shred --force --remove '{}' \;
rm -rf "$dir"
}
done
exit
# set up tests
rm -rf /run/shm/secureblob /tmp/secureblob
mkdir -p /run/shm/secureblob /tmp/secureblob /tmp/secureblob/bob /tmp/secureblob/tom
touch /tmp/secureblob/tom/blob
echo 1 > /tmp/secureblob/tom/time-to-live
touch --date='3 hours ago' /tmp/secureblob/bob/blob
echo 2 > /tmp/secureblob/bob/time-to-live
# end tests

9
alternate/secureblob_up.sh Executable file
View File

@ -0,0 +1,9 @@
#!/bin/bash
url="$1"
[ -z "$url" ] && echo "Must specify URL" && exit 1
file="$2"
[ -z "$file" ] && file="${BASH_SOURCE[0]}"
echo "uploading '$file' to '$url'"
sha1sum < "$file"
curl -s -F "file=@$file" -F 'id=bob' -F 'key=bob' "$url" | sha1sum
curl -s -F 'id=bob' -F 'key=bob' "$url" | sha1sum