From ba9af6ddffe127875338ffbdf2ef57918fcdbf5c Mon Sep 17 00:00:00 2001 From: Joseph Nuthalapati Date: Fri, 29 Jun 2018 17:33:06 +0530 Subject: [PATCH] firstboot: Prompt for secret during firstboot welcome - A freshly installed FreedomBox can be hijacked by a third party and an admin account can be created which can be used to inject malware or simply take over the instance. Password protecting the firstboot step is a good way to avoid this. A secret will be displayed to the user as soon as the Plinth package is installed, which they have to enter during firstboot welcome step. Also, writing this to a file in plinth's home in case the user loses it. - This protection is not applicable for images built by freedom-maker and for Amazon Machine Images. Signed-off-by: Joseph Nuthalapati Reviewed-by: James Valleroy --- debian/control | 1 + debian/postinst | 22 +++++++-- debian/templates | 8 ++++ plinth/modules/first_boot/__init__.py | 14 +++++- plinth/modules/first_boot/forms.py | 47 +++++++++++++++++++ .../templates/firstboot_welcome.html | 23 ++++----- .../templatetags/firstboot_extras.py | 1 - plinth/modules/first_boot/urls.py | 4 +- plinth/modules/first_boot/views.py | 18 +++++-- 9 files changed, 113 insertions(+), 25 deletions(-) create mode 100644 debian/templates create mode 100644 plinth/modules/first_boot/forms.py diff --git a/debian/control b/debian/control index c6128f01c..90898791e 100644 --- a/debian/control +++ b/debian/control @@ -51,6 +51,7 @@ Depends: ${python3:Depends} , ${misc:Depends} , ${plinth:Depends} , adduser + , debconf , augeas-tools , e2fsprogs , fonts-lato diff --git a/debian/postinst b/debian/postinst index 8f2c869cf..06b9ccf8a 100755 --- a/debian/postinst +++ b/debian/postinst @@ -2,6 +2,9 @@ set -e +# Source debconf library. +. /usr/share/debconf/confmodule + daemonuser=plinth daemongroup=plinth @@ -12,11 +15,20 @@ daemongroup=plinth sed -ie 's+-:ALL EXCEPT root fbx (admin) (sudo):ALL+-:ALL EXCEPT root fbx plinth (admin) (sudo):ALL+' /etc/security/access.conf case "$1" in -configure) - addgroup --system --quiet plinth - adduser --system --quiet --ingroup plinth --no-create-home --home /var/lib/plinth plinth - chown -R plinth: /var/lib/plinth /var/log/plinth - ;; + configure) + addgroup --system --quiet plinth + adduser --system --quiet --ingroup plinth --no-create-home --home /var/lib/plinth plinth + chown -R plinth: /var/lib/plinth /var/log/plinth + + if [ ! -e '/var/lib/freedombox/is-freedombox-disk-image' ]; then + umask 377 + cat /dev/urandom | base64 | head -c16 > /var/lib/plinth/firstboot-wizard-secret + chown plinth:plinth /var/lib/plinth/firstboot-wizard-secret + db_subst plinth/firstboot_wizard_secret secret $(cat /var/lib/plinth/firstboot-wizard-secret) + db_input high plinth/firstboot_wizard_secret || true + db_go + fi + ;; esac #DEBHELPER# diff --git a/debian/templates b/debian/templates new file mode 100644 index 000000000..ec65f82bd --- /dev/null +++ b/debian/templates @@ -0,0 +1,8 @@ +Template: plinth/firstboot_wizard_secret +Type: note +Description: FreedomBox first wizard secret - ${secret} + Please save this string. You will be asked to enter this in the first screen + after you launch the FreedomBox interface. In case you lose it, you can find + it in the file /var/lib/plinth/firstboot-wizard-secret. + . + ${secret} diff --git a/plinth/modules/first_boot/__init__.py b/plinth/modules/first_boot/__init__.py index 7a08970ce..eaea06270 100644 --- a/plinth/modules/first_boot/__init__.py +++ b/plinth/modules/first_boot/__init__.py @@ -19,10 +19,11 @@ FreedomBox app for first boot wizard. """ import operator +import os from django.urls import reverse -from plinth import module_loader +from plinth import cfg, module_loader from plinth.signals import post_setup version = 1 @@ -140,3 +141,14 @@ def set_completed(): global _is_completed _is_completed = True kvstore.set('firstboot_completed', 1) + + +def get_secret_file_path(): + """Returns the path to the first boot wizard secret file.""" + return os.path.join(cfg.data_dir, 'firstboot-wizard-secret') + + +def firstboot_wizard_secret_exists(): + """Return whether a firstboot wizard secret exists.""" + secret_file = get_secret_file_path() + return os.path.exists(secret_file) and os.path.getsize(secret_file) > 0 diff --git a/plinth/modules/first_boot/forms.py b/plinth/modules/first_boot/forms.py new file mode 100644 index 000000000..8cd9596f0 --- /dev/null +++ b/plinth/modules/first_boot/forms.py @@ -0,0 +1,47 @@ +# +# This file is part of FreedomBox. +# +# 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 . +# + +from django import forms +from django.utils.translation import ugettext_lazy as _ + +from plinth.modules import first_boot + + +class FirstbootWizardSecretForm(forms.Form): + """Form to collect and validate the first boot wizard secret.""" + secret = forms.CharField( + label='', help_text=_( + 'Enter the secret generated during FreedomBox installation. ' + 'This secret can also be obtained from the file ' + '/var/lib/plinth/firstboot-wizard-secret'), required=False, + widget=forms.PasswordInput(attrs={'placeholder': _('Secret')})) + + def validate_secret(self, secret): + """Match the secret provided by the user with the one + generated during installation. + """ + secret_file_path = first_boot.get_secret_file_path() + with open(secret_file_path) as secret_file: + if secret != secret_file.read().strip(): + self.add_error('secret', 'Invalid secret') + + def clean(self): + """Override clean to add form validation logic.""" + cleaned_data = super().clean() + if first_boot.firstboot_wizard_secret_exists(): + self.validate_secret(cleaned_data.get("secret")) + return cleaned_data diff --git a/plinth/modules/first_boot/templates/firstboot_welcome.html b/plinth/modules/first_boot/templates/firstboot_welcome.html index 23d18110d..e69420aef 100644 --- a/plinth/modules/first_boot/templates/firstboot_welcome.html +++ b/plinth/modules/first_boot/templates/firstboot_welcome.html @@ -27,28 +27,29 @@ .navbar-brand { display: none; } + img.firstboot { + display: block; + max-width: 100%; + } {% endblock %} {% block content %} -

- {{ box_name }} -

+
{% csrf_token %} + {% if show_wizard_password_prompt %} + {{ form|bootstrap }} + {% endif %} +

- -

- {% blocktrans trimmed %} - To complete the setup of your {{ box_name }}, please provide - some basic information. - {% endblocktrans %} -

{% endblock %} diff --git a/plinth/modules/first_boot/templatetags/firstboot_extras.py b/plinth/modules/first_boot/templatetags/firstboot_extras.py index 2484e4c78..0c22c1c31 100644 --- a/plinth/modules/first_boot/templatetags/firstboot_extras.py +++ b/plinth/modules/first_boot/templatetags/firstboot_extras.py @@ -14,7 +14,6 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # - """ Template tags for first boot module. """ diff --git a/plinth/modules/first_boot/urls.py b/plinth/modules/first_boot/urls.py index 61ba83ed5..a8f7cb53a 100644 --- a/plinth/modules/first_boot/urls.py +++ b/plinth/modules/first_boot/urls.py @@ -14,7 +14,6 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # - """ URLs for the First Boot module """ @@ -22,8 +21,7 @@ URLs for the First Boot module from django.conf.urls import url from stronghold.decorators import public -from .views import WelcomeView, CompleteView - +from .views import CompleteView, WelcomeView urlpatterns = [ # Take care of the firstboot middleware when changing URLs diff --git a/plinth/modules/first_boot/views.py b/plinth/modules/first_boot/views.py index 32fd1389a..4bec3b936 100644 --- a/plinth/modules/first_boot/views.py +++ b/plinth/modules/first_boot/views.py @@ -19,21 +19,31 @@ from django import http from django.urls import reverse from django.utils.translation import ugettext as _ from django.views.generic import TemplateView +from django.views.generic.edit import FormView from plinth import network from plinth.modules import first_boot +from .forms import FirstbootWizardSecretForm -class WelcomeView(TemplateView): + +class WelcomeView(FormView): """Show the welcome screen.""" - + form_class = FirstbootWizardSecretForm template_name = 'firstboot_welcome.html' - def post(self, request, *args, **kwargs): - """On POST, mark this step as done and move to next step.""" + def form_valid(self, form): + """If form is valid, mark this step as done and move to next step.""" first_boot.mark_step_done('firstboot_welcome') return http.HttpResponseRedirect(reverse(first_boot.next_step())) + def get_context_data(self, **kwargs): + """Add network connections to context list.""" + context = super().get_context_data(**kwargs) + show_prompt = first_boot.firstboot_wizard_secret_exists() + context['show_wizard_password_prompt'] = show_prompt + return context + class CompleteView(TemplateView): """Show summary after all firstboot setup is done.