Files encryption
This commit is contained in:
Luc Didry
2014-03-06 23:43:59 +01:00
parent 1f93835060
commit 2fae7345d9
12 changed files with 132 additions and 16 deletions

View File

@@ -58,7 +58,8 @@ vi lutim.conf
* broadcast\_message: put some string (not HTML) here and this message will be displayed on all LUTIm pages (not in JSON responses) ;
* allowed\_domains: array of authorized domains for API calls. Example: `['http://1.example.com', 'http://2.example.com']`. If you want to authorize everyone to use the API: `['*']`.
* default\_delay: what is the default time limit for files? Valid values are 0, 1, 7, 30 and 365;
* max\_delay: if defined, the images will be deleted after that delay (in days), even if they were uploaded with "no delay" (or value superior to max\_delay) option and a warning message will be displayed on homepage.
* max\_delay: if defined, the images will be deleted after that delay (in days), even if they were uploaded with "no delay" (or value superior to max\_delay) option and a warning message will be displayed on homepage;
# always\_encrypt: if set to 1, all images will be encrypted.
##Usage
```
@@ -142,6 +143,11 @@ carton exec hypnotoad script/lutim
It may take a few reloads of page before the message is displayed.
##Encryption
LUTIm do encryption on the server if asked to, but does not store the key.
The encryption is made on the server since LUTIm is made to be usable even without javascript. If you want to add client-side encryption for javascript-enabled browsers, patches are welcome.
##API
You can add images by using the API. Here's the parameters of the `POST` request to `/` adress:.
* format: json

View File

@@ -8,3 +8,4 @@ requires 'DateTime';
requires 'Filesys::DiskUsage';
requires 'Switch';
requires 'Data::Validate::URI';
requires 'Crypt::CBC';

View File

@@ -26,6 +26,13 @@ DISTRIBUTIONS
Class::Singleton 1.4
requirements:
ExtUtils::MakeMaker 0
Crypt-CBC-2.33
pathname: L/LD/LDS/Crypt-CBC-2.33.tar.gz
provides:
Crypt::CBC 2.33
requirements:
Digest::MD5 2.00
ExtUtils::MakeMaker 0
DBD-SQLite-1.40
pathname: I/IS/ISHIGAKI/DBD-SQLite-1.40.tar.gz
provides:

View File

@@ -2,6 +2,7 @@ package Lutim;
use Mojo::Base 'Mojolicious';
use Mojo::Util qw(quote);
use LutimModel;
use Crypt::CBC;
$ENV{MOJO_TMPDIR} = 'tmp';
mkdir($ENV{MOJO_TMPDIR}, 0700) unless (-d $ENV{MOJO_TMPDIR});
@@ -14,10 +15,11 @@ sub startup {
my $config = $self->plugin('Config');
# Default values
$config->{provisioning} = 100 unless (defined($config->{provisionning}));
$config->{provisioning} = 100 unless (defined($config->{provisioning}));
$config->{provis_step} = 5 unless (defined($config->{provis_step}));
$config->{length} = 8 unless (defined($config->{length}));
$config->{provisioning} = 100 unless (defined($config->{provisionning}));
$config->{provisioning} = 100 unless (defined($config->{provisioning}));
$config->{provis_step} = 5 unless (defined($config->{provis_step}));
$config->{length} = 8 unless (defined($config->{length}));
$config->{always_encrypt} = 0 unless (defined($config->{always_encrypt}));
die "You need to provide a contact information in lutim.conf !" unless (defined($config->{contact}));
@@ -28,7 +30,7 @@ sub startup {
$self->helper(
render_file => sub {
my $c = shift;
my ($filename, $path, $mediatype, $dl, $expires, $nocache) = @_;
my ($filename, $path, $mediatype, $dl, $expires, $nocache, $key) = @_;
$filename = quote($filename);
@@ -43,18 +45,25 @@ sub startup {
$mediatype =~ s/x-//;
$asset = Mojo::Asset::File->new(path => $path);
my $headers = Mojo::Headers->new();
if ($nocache) {
$headers->add('Cache-Control' => 'no-cache');
$headers->add('Cache-Control' => 'no-cache');
} else {
$headers->add('Expires' => $expires);
$headers->add('Expires' => $expires);
}
$headers->add('Content-Type' => $mediatype.';name='.$filename);
$headers->add('Content-Disposition' => $dl.';filename='.$filename);
$headers->add('Content-Length' => $asset->size);
$c->res->content->headers($headers);
$c->app->log->debug($key);
if ($key) {
$asset = $c->decrypt($key, $path);
} else {
$asset = Mojo::Asset::File->new(path => $path);
}
$c->res->content->asset($asset);
$headers->add('Content-Length' => $asset->size);
return $c->rendered(200);
}
);
@@ -169,6 +178,64 @@ sub startup {
}
);
$self->helper(
crypt => sub {
my $c = shift;
my $upload = shift;
my $filename = shift;
my $key = $c->shortener(8);
my $cipher = Crypt::CBC->new(
-key => $key,
-cipher => 'Blowfish',
-header => 'none',
-iv => 'dupajasi'
);
$cipher->start('encrypting');
my $crypt_asset = Mojo::Asset::File->new;
$crypt_asset->add_chunk($cipher->crypt($upload->slurp));
$crypt_asset->add_chunk($cipher->finish);
my $crypt_upload = Mojo::Upload->new;
$crypt_upload->filename($filename);
$crypt_upload->asset($crypt_asset);
return ($crypt_upload, $key);
}
);
$self->helper(
decrypt => sub {
my $c = shift;
my $key = shift;
my $file = shift;
my $cipher = Crypt::CBC->new(
-key => $key,
-cipher => 'Blowfish',
-header => 'none',
-iv => 'dupajasi'
);
$cipher->start('decrypting');
my $decrypt_asset = Mojo::Asset::File->new;
open(my $f, "<",$file) or die "Unable to read encrypted file: $!";
binmode $f;
while (read($f, my $buffer,1024)) {
$decrypt_asset->add_chunk($cipher->crypt($buffer));
}
$decrypt_asset->add_chunk($cipher->finish) ;
return $decrypt_asset;
}
);
$self->hook(
before_dispatch => sub {
my $c = shift;
@@ -227,6 +294,9 @@ sub startup {
$r->get('/:short')->
to('Controller#short')->
name('short');
$r->get('/:short/:key')->
to('Controller#short');
}
1;

View File

@@ -126,6 +126,10 @@ sub add {
my $filename = unidecode($upload->filename);
my $ext = ($filename =~ m/([^.]+)$/)[0];
my $path = 'files/'.$records[0]->short.'.'.$ext;
my $key;
if ($c->param('crypt') || $c->config->{always_encrypt}) {
($upload, $key) = $c->crypt($upload, $filename);
}
$upload->move_to($path);
$records[0]->update(
path => $path,
@@ -143,7 +147,8 @@ sub add {
$c->app->log->info('[CREATION] '.$c->ip.' pushed '.$filename.' (path: '.$path.')');
# Give url to user
$short = $records[0]->short;
$short = $records[0]->short;
$short .= '/'.$key if (defined($key));
} else {
# Houston, we have a problem
$msg = $c->l('no_more_short', $c->config->{contact});
@@ -201,6 +206,7 @@ sub short {
my $c = shift;
my $short = $c->param('short');
my $touit = $c->param('t');
my $key = $c->param('key');
my $dl = (defined($c->param('dl'))) ? 'attachment' : 'inline';
my @images = LutimModel::Lutim->select('WHERE short = ? AND ENABLED = 1 AND path IS NOT NULL', $short);
@@ -224,10 +230,12 @@ sub short {
my $test;
if (defined($touit)) {
$test = 1;
my $short = $images[0]->short;
$short .= '/'.$key if (defined($key));
$c->render(
template => 'twitter',
layout => undef,
short => $images[0]->short,
short => $short,
filename => $images[0]->filename
);
} else {
@@ -235,7 +243,8 @@ sub short {
my $dt = DateTime->from_epoch( epoch => $expires * 86400 + $images[0]->created_at);
$dt->set_time_zone('GMT');
$expires = $dt->strftime("%a, %d %b %Y %H:%M:%S GMT");
$test = $c->render_file($images[0]->filename, $images[0]->path, $images[0]->mediatype, $dl, $expires, $images[0]->delete_at_first_view);
$test = $c->render_file($images[0]->filename, $images[0]->path, $images[0]->mediatype, $dl, $expires, $images[0]->delete_at_first_view, $key);
}
if ($test != 500) {

View File

@@ -71,6 +71,8 @@ our %Lexicon = (
'delay_30' => '30 days',
'delay_365' => '1 year',
'max_delay' => 'Warning! The maximum time limit for an image is [_1] day(s), even if you choose "no time limit".',
'crypt_image' => 'Encrypt the image (LUTIm does not keep the key).',
'always_encrypt' => 'The images are encrypted on the server (LUTIm does not keep the key).',
);
1;

View File

@@ -71,6 +71,8 @@ our %Lexicon = (
'delay_30' => '30 jours',
'delay_365' => '1 an',
'max_delay' => 'Attention ! Le délai maximal de rétention d\'une image est de [_1] jour(s), même si vous choisissez « pas de limitation de durée ».',
'crypt_image' => 'Chiffrer l\'image (LUTIm ne stocke pas la clé).',
'always_encrypt' => 'Les images sont chiffrées sur le serveur (LUTIm ne stocke pas la clé).',
);
1;

View File

@@ -24,4 +24,5 @@
#allowed_domains => ['http://1.example.com', 'http://2.example.com'], #optional, array of authorized domains for API calls. If you want to authorize everyone to use the API: ['*']
#default_delay => 0, #optional: what is the default time limit for files? Valid values are 0, 1, 7, 30 and 365.
#max_delay => 0, #optional, if defined, the images will be deleted after that delay (in days), even if they were uploaded with "no delay" (or value superior to max\_delay) option and a warning message will be displayed on homepage.
#always_encrypt => 0, #optional, if set to 1, all the images will be encrypted
};

View File

@@ -200,6 +200,7 @@
//});
fd.append('format', 'json');
fd.append('first-view', ($("#first-view").prop('checked')) ? 1 : 0);
fd.append('crypt', ($("#crypt").prop('checked')) ? 1 : 0);
fd.append('delete-day', ($("#delete-day").val()));
widget.settings.onBeforeUpload.call(widget.element, widget.queuePos);

View File

@@ -11,4 +11,5 @@
f.append('format', 'json');
f.append('first-view', ($("#first-view").prop('checked')) ? 1 : 0);
f.append('delete-day', ($("#delete-day").val()));
f.append('crypt', ($("#crypt").prop('checked')) ? 1 : 0);
h.settings.onBeforeUpload.call(h.element,h.queuePos);h.queueRunning=true;c.ajax({url:h.settings.url,type:h.settings.method,dataType:h.settings.dataType,data:f,cache:false,contentType:false,processData:false,forceSync:false,xhr:function(){var i=c.ajaxSettings.xhr();if(i.upload){i.upload.addEventListener("progress",function(m){var l=0;var j=m.loaded||m.position;var k=m.total||e.totalSize;if(m.lengthComputable){l=Math.ceil(j/k*100)}h.settings.onUploadProgress.call(h.element,h.queuePos,l)},false)}return i},success:function(j,i,k){h.settings.onUploadSuccess.call(h.element,h.queuePos,j)},error:function(k,i,j){h.settings.onUploadError.call(h.element,h.queuePos,j)},complete:function(i,j){h.processQueue()}})};c.fn.dmUploader=function(f){return this.each(function(){if(!c.data(this,b)){c.data(this,b,new a(this,f))}})};c(document).on("dragenter",function(f){f.stopPropagation();f.preventDefault()});c(document).on("dragover",function(f){f.stopPropagation();f.preventDefault()});c(document).on("drop",function(f){f.stopPropagation();f.preventDefault()})})(jQuery);

View File

@@ -6,6 +6,9 @@
<strong><%=l('max_delay', max_delay) %></strong>
</div>
% }
% if ($self->config->{always_encrypt}) {
<p><%=l 'always_encrypt' %></p>
% }
% if (defined(flash('short'))) {
<div class="alert alert-success">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
@@ -35,8 +38,12 @@
% }
</select>
<div class="checkbox">
<input type="checkbox" name="first-view"> <%=l 'delete-first' %>
<input type="checkbox" name="crypt_image"> <%=l 'crypt_image' %>
<label>
<input type="checkbox" name="first-view"> <%=l 'delete-first' %>
</label>
<label class="always-encrypt">
<input type="checkbox" name="crypt"> <%=l 'crypt_image' %>
</label>
</div>
</div>
<div class="form-group">
@@ -63,7 +70,9 @@
<div class="checkbox">
<label>
<input type="checkbox" id="first-view"> <%=l 'delete-first' %>
<input type="checkbox" name="crypt_image"> <%=l 'crypt_image' %>
</label>
<label class="always-encrypt">
<input type="checkbox" id="crypt"> <%=l 'crypt_image' %>
</label>
</div>
</div>
@@ -159,6 +168,7 @@
'lutim-file-url' : val,
'format' : 'json',
'first-view' : ($("#first-view").prop('checked')) ? 1 : 0,
'crypt' : ($("#crypt").prop('checked')) ? 1 : 0,
'delete-day' : $("#delete-day").val()
},
success: function(data) {

View File

@@ -35,6 +35,12 @@
ul.no-bullet {
list-style-type: none;
}
% if ($self->config->{always_encrypt}) {
label.always-encrypt {
display: none;
}
% }
% end
%= javascript 'js/jquery-2.1.0.min.js'
%= javascript 'js/bootstrap.min.js'