diff --git a/Gemfile b/Gemfile index d133462..f09323b 100644 --- a/Gemfile +++ b/Gemfile @@ -56,6 +56,7 @@ gem 'inline_svg' gem 'jekyll' gem 'jekyll-data', require: 'jekyll-data', git: 'https://0xacab.org/sutty/jekyll/jekyll-data.git' +gem 'lockbox' gem 'mini_magick' gem 'mobility' gem 'pg' diff --git a/Gemfile.lock b/Gemfile.lock index e47d6bc..efe7f9c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -262,6 +262,7 @@ GEM rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) ruby_dep (~> 1.2) + lockbox (0.4.6) lograge (0.11.2) actionpack (>= 4) activesupport (>= 4) @@ -540,6 +541,7 @@ DEPENDENCIES jekyll-data! letter_opener listen (>= 3.0.5, < 3.2) + lockbox lograge memory_profiler mini_magick diff --git a/app/models/metadata_encrypted_text.rb b/app/models/metadata_encrypted_text.rb new file mode 100644 index 0000000..4a20886 --- /dev/null +++ b/app/models/metadata_encrypted_text.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +# Íbamos a usar OpenSSL pero esto es más simple de implementar y no +# tenemos que usar cifrado compatible con JavaScript. +class MetadataEncryptedText < MetadataText + # Decifra el valor si está guardado en el sitio + def value + self[:value] ||= if (v = document.data.dig(name.to_s)) + box.decrypt_str v + else + default_value + end + rescue Lockbox::DecryptionError => e + ExceptionNotifier.notify_exception(e) + + self[:value] ||= I18n.t('lockbox.help.decryption_error') + end + + # Cifra el valor antes de guardarlo + def save + self[:value] = box.encrypt sanitize(value) + + true + end + + private + + # Genera una lockbox a partir de la llave privada del sitio + # + # @return [Lockbox] + def box + @box ||= Lockbox.new key: site.private_key, padding: true, encode: true + end +end diff --git a/app/models/site.rb b/app/models/site.rb index 6e31c13..6d2226e 100644 --- a/app/models/site.rb +++ b/app/models/site.rb @@ -7,6 +7,12 @@ class Site < ApplicationRecord include Site::Forms include Site::FindAndReplace + # Cifrar la llave privada que cifra y decifra campos ocultos. Sutty + # tiene acceso pero los datos se guardan cifrados en el sitio. Esto + # protege información privada en repositorios públicos, pero no la + # protege de acceso al panel de Sutty! + encrypts :private_key + # TODO: Hacer que los diferentes tipos de deploy se auto registren # @see app/services/site_service.rb DEPLOYS = %i[local www zip hidden_service].freeze diff --git a/app/views/posts/attribute_ro/_encrypted_text.haml b/app/views/posts/attribute_ro/_encrypted_text.haml new file mode 100644 index 0000000..6330fbe --- /dev/null +++ b/app/views/posts/attribute_ro/_encrypted_text.haml @@ -0,0 +1,7 @@ +%tr{ id: attribute } + %th + %abbr{ title: t('lockbox.help.description') } + 🔒 + %span.sr-only= t('lockbox.help.title') + = post_label_t(attribute, post: post) + %td{ dir: dir, lang: locale }= metadata.value diff --git a/app/views/posts/attributes/_encrypted_text.haml b/app/views/posts/attributes/_encrypted_text.haml new file mode 100644 index 0000000..1d732e0 --- /dev/null +++ b/app/views/posts/attributes/_encrypted_text.haml @@ -0,0 +1,11 @@ +.form-group + = label_tag "post_#{attribute}" do + %abbr{ title: t('lockbox.help.description') } + 🔒 + %span.sr-only= t('lockbox.help.title') + = post_label_t(attribute, post: post) + = text_area_tag "post[#{attribute}]", metadata.value, + dir: dir, lang: locale, + **field_options(attribute, metadata) + = render 'posts/attribute_feedback', + post: post, attribute: attribute, metadata: metadata diff --git a/config/initializers/lockbox.rb b/config/initializers/lockbox.rb new file mode 100644 index 0000000..c54cedd --- /dev/null +++ b/config/initializers/lockbox.rb @@ -0,0 +1 @@ +Lockbox.master_key = Rails.application.credentials.lockbox_master_key diff --git a/config/locales/en.yml b/config/locales/en.yml index b042c2c..553a247 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -514,3 +514,8 @@ en: title: '404: Page not found :(' description: "You're reading this message because the page you wanted doesn't exist." button: 'Back to panel' + lockbox: + help: + title: Encrypted content + description: The field contents are encrypted before being stored and won't be available on the public website or its source code. You can save private information here and it will only be readable to this site's users through Sutty's panel. + decryption_error: There was an error trying to decrypt the content, Sutty's team has been notified! diff --git a/config/locales/es.yml b/config/locales/es.yml index d6877e8..1dfbd7f 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -527,3 +527,8 @@ es: title: '404: Página no encontrada :(' description: 'Estás leyendo este error porque la página que quisiste acceder no existe.' button: 'Volver al panel' + lockbox: + help: + title: Contenido cifrado + description: El contenido de este campo se guarda cifrado y no estará disponible en el sitio ni en su código fuente. Puedes guardar información privada aquí y sólo estará disponible para quienes tengan acceso a ese sitio en el panel de Sutty. + decryption_error: Hubo un error al decifrar la información, ¡el equipo de Sutty ya fue notificado! diff --git a/db/migrate/20200810230944_add_priv_key_to_sites.rb b/db/migrate/20200810230944_add_priv_key_to_sites.rb new file mode 100644 index 0000000..ee555a1 --- /dev/null +++ b/db/migrate/20200810230944_add_priv_key_to_sites.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +# Agrega las llaves privadas cifradas a cada sitio +class AddPrivKeyToSites < ActiveRecord::Migration[6.0] + def up + add_column :sites, :private_key_ciphertext, :string + + Site.find_each do |site| + site.update_attribute :private_key, Lockbox.generate_key + end + end + + def down + remove_column :sites, :private_key_ciphertext + end +end diff --git a/db/schema.rb b/db/schema.rb index 9cc2bb7..5d917ff 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -12,8 +12,8 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20_200_616_133_218) do - # Could not dump table "access_logs" because of following StandardError +ActiveRecord::Schema.define(version: 20_200_810_230_944) do + # Could not dump table 'access_logs' because of following StandardError # Unknown type '' for column 'id' create_table 'action_text_rich_texts', force: :cascade do |t| @@ -58,7 +58,7 @@ ActiveRecord::Schema.define(version: 20_200_616_133_218) do t.index ['deploy_id'], name: 'index_build_stats_on_deploy_id' end - # Could not dump table "csp_reports" because of following StandardError + # Could not dump table 'csp_reports' because of following StandardError # Unknown type 'uuid' for column 'id' create_table 'deploys', force: :cascade do |t| @@ -157,6 +157,7 @@ ActiveRecord::Schema.define(version: 20_200_616_133_218) do t.string 'title' t.boolean 'colaboracion_anonima', default: false t.boolean 'contact', default: false + t.string 'private_key_ciphertext' t.index ['design_id'], name: 'index_sites_on_design_id' t.index ['licencia_id'], name: 'index_sites_on_licencia_id' t.index ['name'], name: 'index_sites_on_name', unique: true