Add a simple pawword sharing module
This commit is contained in:
parent
8c601c23a2
commit
d7af4cd80c
17 changed files with 502 additions and 2 deletions
1
Pipfile
1
Pipfile
|
@ -9,6 +9,7 @@ django-bulma = "*"
|
|||
django-npb = "*"
|
||||
markdown = "*"
|
||||
python-decouple = "*"
|
||||
cryptography = ">=2.7,<2.8"
|
||||
|
||||
[dev-packages]
|
||||
gunicorn = "*"
|
||||
|
|
|
@ -49,6 +49,7 @@ INSTALLED_APPS = [
|
|||
"npb.apps.NpbConfig",
|
||||
"nsfw.apps.NsfwConfig",
|
||||
"pages.apps.PagesConfig",
|
||||
"pwdb.apps.PwdbConfig",
|
||||
"static_extra.apps.KhaganatStaticFilesConfig",
|
||||
]
|
||||
|
||||
|
|
|
@ -25,8 +25,9 @@ urlpatterns += i18n_patterns(
|
|||
path("", index),
|
||||
path("admin/", admin.site.urls),
|
||||
path("account/", include("neluser.urls")),
|
||||
path("page/", include("pages.urls")),
|
||||
path("paste/", include("npb.urls", namespace="npb")),
|
||||
path("chat/", include("chat.urls")),
|
||||
path("nsfw/", include("nsfw.urls")),
|
||||
path("page/", include("pages.urls")),
|
||||
path("paste/", include("npb.urls", namespace="npb")),
|
||||
path("password_share/", include("pwdb.urls")),
|
||||
)
|
||||
|
|
0
pwdb/__init__.py
Normal file
0
pwdb/__init__.py
Normal file
26
pwdb/admin.py
Normal file
26
pwdb/admin.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
from django.contrib import admin
|
||||
from .models import SharedPassword, SharedPasswordAccess
|
||||
from .forms import NewSharedPasswordForm, EditSharedPasswordForm
|
||||
|
||||
|
||||
class SharedPasswordAdmin(admin.ModelAdmin):
|
||||
form = NewSharedPasswordForm
|
||||
exclude = ["iv", "encrypted_password"]
|
||||
list_display = ("name", "users")
|
||||
|
||||
def get_form(self, request, obj=None, **kwargs):
|
||||
if obj is None:
|
||||
kwargs["form"] = NewSharedPasswordForm
|
||||
else:
|
||||
kwargs["form"] = EditSharedPasswordForm
|
||||
return super().get_form(request, obj, **kwargs)
|
||||
|
||||
|
||||
admin.site.register(SharedPassword, SharedPasswordAdmin)
|
||||
|
||||
|
||||
class SharedPasswordAccessAdmin(admin.ModelAdmin):
|
||||
list_display = ("password", "user")
|
||||
|
||||
|
||||
admin.site.register(SharedPasswordAccess, SharedPasswordAccessAdmin)
|
5
pwdb/apps.py
Normal file
5
pwdb/apps.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class PwdbConfig(AppConfig):
|
||||
name = "pwdb"
|
75
pwdb/forms.py
Normal file
75
pwdb/forms.py
Normal file
|
@ -0,0 +1,75 @@
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
from django import forms
|
||||
from .models import SharedPassword
|
||||
|
||||
|
||||
class AuthForm(forms.Form):
|
||||
pwdb_check = forms.CharField(widget=forms.PasswordInput, label="")
|
||||
|
||||
|
||||
class NewSharedPasswordForm(forms.ModelForm):
|
||||
name = forms.CharField(max_length=512, label=_("Name"))
|
||||
url = forms.CharField(
|
||||
max_length=512, widget=forms.URLInput, required=False, label="URL"
|
||||
)
|
||||
description = forms.CharField(
|
||||
widget=forms.Textarea, required=False, label=_("Description")
|
||||
)
|
||||
password = forms.CharField(
|
||||
max_length=1024, widget=forms.PasswordInput, label=_("Password")
|
||||
)
|
||||
|
||||
def save_m2m(self):
|
||||
pass
|
||||
|
||||
def save(self, commit=True):
|
||||
if self.errors:
|
||||
raise ValueError(
|
||||
"The %s could not be %s because the data didn't validate."
|
||||
% (
|
||||
self.instance._meta.object_name,
|
||||
"created" if self.instance._state.adding else "changed",
|
||||
)
|
||||
)
|
||||
password = SharedPassword.new(
|
||||
self.cleaned_data["name"], self.cleaned_data["password"]
|
||||
)
|
||||
if self.cleaned_data["url"]:
|
||||
password.url = self.cleaned_data["url"]
|
||||
if self.cleaned_data["description"]:
|
||||
password.description = self.cleaned_data["description"]
|
||||
password.save()
|
||||
return password
|
||||
|
||||
class Meta:
|
||||
model = SharedPassword
|
||||
exclude = ["iv", "encrypted_password"]
|
||||
|
||||
|
||||
class EditSharedPasswordForm(forms.ModelForm):
|
||||
name = forms.CharField(max_length=512, label=_("Name"))
|
||||
password = forms.CharField(
|
||||
max_length=1024, widget=forms.PasswordInput, required=False, label=_("Password")
|
||||
)
|
||||
|
||||
def save_m2m(self):
|
||||
pass
|
||||
|
||||
def save(self, commit=True):
|
||||
if self.errors:
|
||||
raise ValueError(
|
||||
"The %s could not be %s because the data didn't validate."
|
||||
% (
|
||||
self.instance._meta.object_name,
|
||||
"created" if self.instance._state.adding else "changed",
|
||||
)
|
||||
)
|
||||
password = self.instance
|
||||
if self.cleaned_data["password"]:
|
||||
password.set_password(self.cleaned_data["password"])
|
||||
password.save()
|
||||
return password
|
||||
|
||||
class Meta:
|
||||
model = SharedPassword
|
||||
exclude = ["iv", "encrypted_password"]
|
73
pwdb/locale/en/LC_MESSAGES/django.po
Normal file
73
pwdb/locale/en/LC_MESSAGES/django.po
Normal file
|
@ -0,0 +1,73 @@
|
|||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: 1.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-07-27 16:41+0200\n"
|
||||
"PO-Revision-Date: 2019-07-27 16:41+0200\n"
|
||||
"Last-Translator: Khaganat <assoc@khaganat.net>\n"
|
||||
"Language-Team: Khaganat <assoc@khaganat.net>\n"
|
||||
"Language: en\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: forms.py:11 forms.py:46 templates/pwdb/list_passwords.html:12
|
||||
msgid "Name"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:13 templates/pwdb/list_passwords.html:13
|
||||
msgid "Description"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:15 forms.py:48 templates/pwdb/list_passwords.html:15
|
||||
msgid "Password"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:102
|
||||
msgid "shared_password"
|
||||
msgstr "Shared password"
|
||||
|
||||
#: models.py:103 templates/pwdb/list_passwords.html:4
|
||||
msgid "shared_passwords"
|
||||
msgstr "Shared passwords"
|
||||
|
||||
#: models.py:116
|
||||
msgid "shared_password_access"
|
||||
msgstr "Shared password access"
|
||||
|
||||
#: models.py:117
|
||||
msgid "shared_passwords_access"
|
||||
msgstr "Shared passwords access"
|
||||
|
||||
#: templates/pwdb/authenticate.html:5
|
||||
msgid "authenticate"
|
||||
msgstr ""
|
||||
|
||||
#: templates/pwdb/authenticate.html:10
|
||||
msgid "safety_enter_password"
|
||||
msgstr "For safety reasons, please enter your password."
|
||||
|
||||
#: templates/pwdb/authenticate.html:15
|
||||
msgid "send"
|
||||
msgstr ""
|
||||
|
||||
#: templates/pwdb/list_passwords.html:16
|
||||
msgid "Actions"
|
||||
msgstr ""
|
||||
|
||||
#: templates/pwdb/list_passwords.html:24
|
||||
msgid "view_website"
|
||||
msgstr "View website"
|
||||
|
||||
#: templates/pwdb/list_passwords.html:26
|
||||
msgid "copy_password"
|
||||
msgstr "Copy"
|
||||
|
||||
#: templates/pwdb/list_passwords.html:27
|
||||
msgid "show_password"
|
||||
msgstr "Show"
|
||||
|
||||
#: templates/pwdb/list_passwords.html:28
|
||||
msgid "hide_password"
|
||||
msgstr "Hide"
|
73
pwdb/locale/fr/LC_MESSAGES/django.po
Normal file
73
pwdb/locale/fr/LC_MESSAGES/django.po
Normal file
|
@ -0,0 +1,73 @@
|
|||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: 1.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-07-27 16:41+0200\n"
|
||||
"PO-Revision-Date: 2019-07-27 16:41+0200\n"
|
||||
"Last-Translator: Khaganat <assoc@khaganat.net>\n"
|
||||
"Language-Team: Khaganat <assoc@khaganat.net>\n"
|
||||
"Language: fr\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#: forms.py:11 forms.py:46 templates/pwdb/list_passwords.html:12
|
||||
msgid "Name"
|
||||
msgstr "Nom"
|
||||
|
||||
#: forms.py:13 templates/pwdb/list_passwords.html:13
|
||||
msgid "Description"
|
||||
msgstr "Description"
|
||||
|
||||
#: forms.py:15 forms.py:48 templates/pwdb/list_passwords.html:15
|
||||
msgid "Password"
|
||||
msgstr "Mot de passe"
|
||||
|
||||
#: models.py:102
|
||||
msgid "shared_password"
|
||||
msgstr "Mot de passe partagé"
|
||||
|
||||
#: models.py:103 templates/pwdb/list_passwords.html:4
|
||||
msgid "shared_passwords"
|
||||
msgstr "Mots de passe partagés"
|
||||
|
||||
#: models.py:116
|
||||
msgid "shared_password_access"
|
||||
msgstr "Accès au mot de passe partagé"
|
||||
|
||||
#: models.py:117
|
||||
msgid "shared_passwords_access"
|
||||
msgstr "Accès aux mots de passe partagés"
|
||||
|
||||
#: templates/pwdb/authenticate.html:5
|
||||
msgid "authenticate"
|
||||
msgstr "Authentification"
|
||||
|
||||
#: templates/pwdb/authenticate.html:10
|
||||
msgid "safety_enter_password"
|
||||
msgstr "À des fins de sécurité, veuillez entrer votre mot de passe."
|
||||
|
||||
#: templates/pwdb/authenticate.html:15
|
||||
msgid "send"
|
||||
msgstr "Envoyer"
|
||||
|
||||
#: templates/pwdb/list_passwords.html:16
|
||||
msgid "Actions"
|
||||
msgstr "Actions"
|
||||
|
||||
#: templates/pwdb/list_passwords.html:24
|
||||
msgid "view_website"
|
||||
msgstr "Voir le site web"
|
||||
|
||||
#: templates/pwdb/list_passwords.html:26
|
||||
msgid "copy_password"
|
||||
msgstr "Copier"
|
||||
|
||||
#: templates/pwdb/list_passwords.html:27
|
||||
msgid "show_password"
|
||||
msgstr "Montrer"
|
||||
|
||||
#: templates/pwdb/list_passwords.html:28
|
||||
msgid "hide_password"
|
||||
msgstr "Masquer"
|
45
pwdb/migrations/0001_initial.py
Normal file
45
pwdb/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,45 @@
|
|||
# Generated by Django 2.2.3 on 2019-07-27 15:16
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SharedPassword',
|
||||
fields=[
|
||||
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=512)),
|
||||
('url', models.CharField(blank=True, max_length=512)),
|
||||
('description', models.TextField(blank=True)),
|
||||
('iv', models.BinaryField(max_length=16)),
|
||||
('encrypted_password', models.BinaryField(max_length=2048)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'shared_password',
|
||||
'verbose_name_plural': 'shared_passwords',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SharedPasswordAccess',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('password', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pwdb.SharedPassword')),
|
||||
('user', models.ForeignKey(limit_choices_to={'is_staff': True}, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'shared_password_access',
|
||||
'verbose_name_plural': 'shared_passwords_access',
|
||||
},
|
||||
),
|
||||
]
|
0
pwdb/migrations/__init__.py
Normal file
0
pwdb/migrations/__init__.py
Normal file
117
pwdb/models.py
Normal file
117
pwdb/models.py
Normal file
|
@ -0,0 +1,117 @@
|
|||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
||||
from cryptography.hazmat.primitives import hashes, padding
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from neluser.models import NelUser
|
||||
import secrets
|
||||
import uuid
|
||||
|
||||
KEY_LENGTH = 32
|
||||
IV_LENGTH = 16
|
||||
BLOCK_SIZE = 128
|
||||
ENCODING = "UTF-8"
|
||||
|
||||
|
||||
class SharedPassword(models.Model):
|
||||
uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
name = models.CharField(max_length=512)
|
||||
url = models.CharField(max_length=512, blank=True)
|
||||
description = models.TextField(blank=True)
|
||||
iv = models.BinaryField(max_length=IV_LENGTH)
|
||||
encrypted_password = models.BinaryField(max_length=2048)
|
||||
|
||||
@staticmethod
|
||||
def get_key(pass_uuid, key=None):
|
||||
backend = default_backend()
|
||||
hkdf = HKDF(
|
||||
algorithm=hashes.SHA256(),
|
||||
length=KEY_LENGTH,
|
||||
salt=pass_uuid.bytes,
|
||||
backend=backend,
|
||||
info=None,
|
||||
)
|
||||
key = key or settings.SECRET_KEY
|
||||
key = bytes(key, encoding=ENCODING)
|
||||
return hkdf.derive(key)
|
||||
|
||||
@staticmethod
|
||||
def get_cipher(key, iv):
|
||||
backend = default_backend()
|
||||
return Cipher(algorithms.Camellia(key), modes.CBC(iv), backend=backend)
|
||||
|
||||
@staticmethod
|
||||
def padd_password(clear_password):
|
||||
clear_password = bytes(clear_password, encoding=ENCODING)
|
||||
padder = padding.PKCS7(BLOCK_SIZE).padder()
|
||||
padded_password = padder.update(clear_password) + padder.finalize()
|
||||
return padded_password
|
||||
|
||||
@staticmethod
|
||||
def unpadd_password(clear_password):
|
||||
unpadder = padding.PKCS7(BLOCK_SIZE).unpadder()
|
||||
unpadded_password = unpadder.update(clear_password) + unpadder.finalize()
|
||||
return unpadded_password.decode()
|
||||
|
||||
@staticmethod
|
||||
def new(name, clear_password):
|
||||
password = SharedPassword(
|
||||
uuid=uuid.uuid4(),
|
||||
name=name,
|
||||
url="",
|
||||
description="",
|
||||
iv=secrets.token_bytes(IV_LENGTH),
|
||||
encrypted_password=b"",
|
||||
)
|
||||
password.set_password(clear_password)
|
||||
return password
|
||||
|
||||
def set_password(self, clear_password):
|
||||
clear_password = SharedPassword.padd_password(clear_password)
|
||||
key = SharedPassword.get_key(self.uuid)
|
||||
cipher = SharedPassword.get_cipher(key, self.iv)
|
||||
encryptor = cipher.encryptor()
|
||||
self.encrypted_password = (
|
||||
encryptor.update(clear_password) + encryptor.finalize()
|
||||
)
|
||||
|
||||
def decrypt_password(self):
|
||||
key = SharedPassword.get_key(self.uuid)
|
||||
cipher = SharedPassword.get_cipher(key, self.iv)
|
||||
decryptor = cipher.decryptor()
|
||||
clear_password = (
|
||||
decryptor.update(self.encrypted_password) + decryptor.finalize()
|
||||
)
|
||||
clear_password = SharedPassword.unpadd_password(clear_password)
|
||||
return clear_password
|
||||
|
||||
def users(self):
|
||||
lst = (
|
||||
SharedPasswordAccess.objects.select_related("user")
|
||||
.filter(password=self)
|
||||
.order_by("user__email")
|
||||
)
|
||||
return [e.user for e in lst]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("shared_password")
|
||||
verbose_name_plural = _("shared_passwords")
|
||||
|
||||
|
||||
class SharedPasswordAccess(models.Model):
|
||||
user = models.ForeignKey(
|
||||
NelUser, on_delete=models.CASCADE, limit_choices_to={"is_staff": True}
|
||||
)
|
||||
password = models.ForeignKey(SharedPassword, on_delete=models.CASCADE)
|
||||
|
||||
def __str__(self):
|
||||
return "{}: {}".format(self.password, self.user)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("shared_password_access")
|
||||
verbose_name_plural = _("shared_passwords_access")
|
17
pwdb/templates/pwdb/authenticate.html
Normal file
17
pwdb/templates/pwdb/authenticate.html
Normal file
|
@ -0,0 +1,17 @@
|
|||
{% extends "khaganat/centered_dialog.html" %}
|
||||
{% load bulma_tags %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "authenticate"|capfirst %}{% endblock %}
|
||||
{% block dialog_class %}is-link{% endblock %}
|
||||
|
||||
{% block dialog %}
|
||||
<p>
|
||||
{% trans "safety_enter_password" %}
|
||||
</p>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<button type="submit" class="button is-link">{% trans "send"|capfirst %}</button>
|
||||
</form>
|
||||
{% endblock %}
|
35
pwdb/templates/pwdb/list_passwords.html
Normal file
35
pwdb/templates/pwdb/list_passwords.html
Normal file
|
@ -0,0 +1,35 @@
|
|||
{% extends "khaganat/base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "shared_passwords" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="content-bloc">
|
||||
<div class="table-container">
|
||||
<table class="table is-striped is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Name" %}</th>
|
||||
<th>{% trans "Description" %}</th>
|
||||
<th>URL</th>
|
||||
<th>{% trans "Password" %}</th>
|
||||
<th colspan="3">{% trans "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for pass in passwords %}
|
||||
<tr>
|
||||
<td class="has-text-weight-semibold">{{ pass.name }}</td>
|
||||
<td class="has-text-weight-light">{{ pass.description }}</td>
|
||||
<td>{% if pass.url %}<a href="{{ pass.url }}" target="_blank">{{ pass.url|truncatechars:30 }}</a>{% endif %}</td>
|
||||
<td><input class="input" type="password" id="password_{{ pass.uuid }}" name="password_{{ pass.uuid }}" value="{{ pass.decrypt_password }}" /></td>
|
||||
<td><a class="button is-primary" onclick="navigator.clipboard.writeText(document.getElementById('password_{{ pass.uuid }}').value).then(function() { console.log('Password copied to clipboard'); }, function(err) { console.error('Unable to copy password to clipboard'); });">{% trans "copy_password" %}</a></td>
|
||||
<td><a id="show_{{ pass.uuid }}" class="button is-warning" onclick="document.getElementById('password_{{ pass.uuid }}').type = 'text';">{% trans "show_password" %}</a></td>
|
||||
<td><a id="hide_{{ pass.uuid }}" class="button is-success" onclick="document.getElementById('password_{{ pass.uuid }}').type = 'password';">{% trans "hide_password" %}</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
3
pwdb/tests.py
Normal file
3
pwdb/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
5
pwdb/urls.py
Normal file
5
pwdb/urls.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
|
||||
urlpatterns = [path("", views.list_passwords, name="pwdb")]
|
23
pwdb/views.py
Normal file
23
pwdb/views.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
from django.contrib.admin.views.decorators import staff_member_required
|
||||
from django.contrib.auth import authenticate
|
||||
from django.shortcuts import render
|
||||
from .models import SharedPasswordAccess
|
||||
from .forms import AuthForm
|
||||
|
||||
|
||||
@staff_member_required
|
||||
def list_passwords(request):
|
||||
try:
|
||||
pwd = request.POST["pwdb_check"]
|
||||
user = authenticate(username=request.user, password=pwd)
|
||||
assert user is not None
|
||||
lst = (
|
||||
SharedPasswordAccess.objects.select_related("password")
|
||||
.filter(user__pk=user.pk)
|
||||
.order_by("password__name")
|
||||
)
|
||||
ctx = {"passwords": [e.password for e in lst]}
|
||||
return render(request, "pwdb/list_passwords.html", ctx)
|
||||
except (KeyError, AssertionError):
|
||||
ctx = {"form": AuthForm()}
|
||||
return render(request, "pwdb/authenticate.html", ctx)
|
Loading…
Reference in a new issue