diff --git a/alternate/readme.md b/alternate/readme.md new file mode 100644 index 0000000..5229830 --- /dev/null +++ b/alternate/readme.md @@ -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 diff --git a/alternate/secureblob.php b/alternate/secureblob.php new file mode 100644 index 0000000..fec6781 --- /dev/null +++ b/alternate/secureblob.php @@ -0,0 +1,258 @@ +. +*/ +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'
\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'
\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'
\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'
\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'
\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
\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
\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'
\n"; +?> \ No newline at end of file diff --git a/alternate/secureblob_cron.sh b/alternate/secureblob_cron.sh new file mode 100755 index 0000000..c0c1b09 --- /dev/null +++ b/alternate/secureblob_cron.sh @@ -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 . + +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 diff --git a/alternate/secureblob_up.sh b/alternate/secureblob_up.sh new file mode 100755 index 0000000..b044435 --- /dev/null +++ b/alternate/secureblob_up.sh @@ -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