diff --git a/README.md b/README.md index ef454e0..3c2594d 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/cpanfile b/cpanfile index 895a1dd..661c8f3 100644 --- a/cpanfile +++ b/cpanfile @@ -8,3 +8,4 @@ requires 'DateTime'; requires 'Filesys::DiskUsage'; requires 'Switch'; requires 'Data::Validate::URI'; +requires 'Crypt::CBC'; diff --git a/cpanfile.snapshot b/cpanfile.snapshot index 67c4ba6..fc3dbb5 100644 --- a/cpanfile.snapshot +++ b/cpanfile.snapshot @@ -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: diff --git a/lib/Lutim.pm b/lib/Lutim.pm index 15ab45b..9b7ffe0 100644 --- a/lib/Lutim.pm +++ b/lib/Lutim.pm @@ -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; diff --git a/lib/Lutim/Controller.pm b/lib/Lutim/Controller.pm index 7880ba0..0e0b83f 100644 --- a/lib/Lutim/Controller.pm +++ b/lib/Lutim/Controller.pm @@ -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) { diff --git a/lib/Lutim/I18N/en.pm b/lib/Lutim/I18N/en.pm index 224936c..13c3ec4 100644 --- a/lib/Lutim/I18N/en.pm +++ b/lib/Lutim/I18N/en.pm @@ -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; diff --git a/lib/Lutim/I18N/fr.pm b/lib/Lutim/I18N/fr.pm index d4a2737..e3b9393 100644 --- a/lib/Lutim/I18N/fr.pm +++ b/lib/Lutim/I18N/fr.pm @@ -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; diff --git a/lutim.conf.template b/lutim.conf.template index ad79c73..051c380 100644 --- a/lutim.conf.template +++ b/lutim.conf.template @@ -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 }; diff --git a/public/js/dmuploader.js b/public/js/dmuploader.js index 92f4112..6a723a8 100644 --- a/public/js/dmuploader.js +++ b/public/js/dmuploader.js @@ -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); diff --git a/public/js/dmuploader.min.js b/public/js/dmuploader.min.js index 463a08d..4eb1a24 100644 --- a/public/js/dmuploader.min.js +++ b/public/js/dmuploader.min.js @@ -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); diff --git a/templates/index.html.ep b/templates/index.html.ep index e755721..4da4b3a 100644 --- a/templates/index.html.ep +++ b/templates/index.html.ep @@ -6,6 +6,9 @@ <%=l('max_delay', max_delay) %> % } +% if ($self->config->{always_encrypt}) { +
<%=l 'always_encrypt' %>
+% } % if (defined(flash('short'))) {