@@ -1 +0,0 @@ | |||
Subproject commit 670e4b39c06416a1f3946a7d97074dc6f42751b7 |
@@ -0,0 +1,17 @@ | |||
# editorconfig.org | |||
root = true | |||
[*] | |||
charset = utf-8 | |||
end_of_line = lf | |||
indent_size = 2 | |||
indent_style = space | |||
insert_final_newline = true | |||
trim_trailing_whitespace = true | |||
[*.md] | |||
trim_trailing_whitespace = false | |||
[*.py] | |||
indent_size = 4 |
@@ -0,0 +1,94 @@ | |||
# Created by .ignore support plugin (hsz.mobi) | |||
### Python template | |||
# Byte-compiled / optimized / DLL files | |||
__pycache__/ | |||
*.py[cod] | |||
*$py.class | |||
# C extensions | |||
*.so | |||
# Distribution / packaging | |||
.Python | |||
env/ | |||
build/ | |||
develop-eggs/ | |||
dist/ | |||
downloads/ | |||
eggs/ | |||
.eggs/ | |||
lib/ | |||
lib64/ | |||
parts/ | |||
sdist/ | |||
var/ | |||
*.egg-info/ | |||
.installed.cfg | |||
*.egg | |||
# PyInstaller | |||
# Usually these files are written by a python script from a template | |||
# before PyInstaller builds the exe, so as to inject date/other infos into it. | |||
*.manifest | |||
*.spec | |||
# Installer logs | |||
pip-log.txt | |||
pip-delete-this-directory.txt | |||
# Unit test / coverage reports | |||
htmlcov/ | |||
.tox/ | |||
.coverage | |||
.coverage.* | |||
.cache | |||
nosetests.xml | |||
coverage.xml | |||
*,cover | |||
.hypothesis/ | |||
# Translations | |||
*.mo | |||
*.pot | |||
# Django stuff: | |||
*.log | |||
local_settings.py | |||
# Flask stuff: | |||
instance/ | |||
.webassets-cache | |||
# Scrapy stuff: | |||
.scrapy | |||
# Sphinx documentation | |||
docs/_build/ | |||
# PyBuilder | |||
target/ | |||
# IPython Notebook | |||
.ipynb_checkpoints | |||
# pyenv | |||
.python-version | |||
# celery beat schedule file | |||
celerybeat-schedule | |||
# dotenv | |||
.env | |||
# virtualenv | |||
venv/ | |||
ENV/ | |||
# Spyder project settings | |||
.spyderproject | |||
# Rope project settings | |||
.ropeproject | |||
/db.sqlite3 | |||
www/ |
@@ -0,0 +1,13 @@ | |||
dist: trusty | |||
sudo: required | |||
language: python | |||
python: | |||
- 3.5 | |||
addons: | |||
postgresql: "9.5" | |||
services: | |||
- postgresql | |||
env: | |||
- DATABASE_ENGINE=django.db.backends.postgresql_psycopg2 DATABASE_NAME=postgres DATABASE_USER=postgres DATABASE_PASSWORD= DATABASE_HOST=localhost DATABASE_PORT=5432 | |||
install: pip install -r requirements.txt | |||
script: python manage.py test |
@@ -0,0 +1,19 @@ | |||
FROM python:3.5-alpine | |||
RUN apk add --no-cache supervisor netcat-openbsd postgresql-dev gcc python3-dev musl-dev | |||
RUN mkdir /backend | |||
WORKDIR /backend | |||
COPY requirements.txt /backend/ | |||
RUN pip install --upgrade pip | |||
RUN pip install -r requirements.txt | |||
COPY api/ /backend/api/ | |||
COPY lesspass/ /backend/lesspass/ | |||
COPY manage.py /backend/manage.py | |||
COPY entrypoint.sh / | |||
ENTRYPOINT ["/entrypoint.sh"] | |||
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf | |||
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] |
@@ -0,0 +1,24 @@ | |||
# LessPass backend | |||
REST API used by [lesspass-pure](https://github.com/lesspass/pure) to store password profiles | |||
* python 3 | |||
* django rest framework | |||
* django rest framework jwt | |||
* djoser | |||
* postgresql | |||
* gunicorn | |||
## Tests | |||
pip install -r requirements.txt | |||
python manage.py test | |||
## License | |||
This project is licensed under the terms of the GNU GPLv3. | |||
## Issues | |||
report issues on [LessPass project](https://github.com/lesspass/lesspass/issues) |
@@ -0,0 +1,84 @@ | |||
from api import models | |||
from api.models import LessPassUser | |||
from django import forms | |||
from django.contrib import admin | |||
from django.contrib.auth.models import Group | |||
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin | |||
from django.contrib.auth.forms import ReadOnlyPasswordHashField | |||
from django.db.models import Count | |||
class UserCreationForm(forms.ModelForm): | |||
password1 = forms.CharField(label='Password', widget=forms.PasswordInput) | |||
password2 = forms.CharField(label='Password confirmation', widget=forms.PasswordInput) | |||
class Meta: | |||
model = LessPassUser | |||
fields = ('email',) | |||
def clean_password2(self): | |||
password1 = self.cleaned_data.get("password1") | |||
password2 = self.cleaned_data.get("password2") | |||
if password1 and password2 and password1 != password2: | |||
raise forms.ValidationError("Passwords don't match") | |||
return password2 | |||
def save(self, commit=True): | |||
user = super(UserCreationForm, self).save(commit=False) | |||
user.set_password(self.cleaned_data["password1"]) | |||
if commit: | |||
user.save() | |||
return user | |||
class UserChangeForm(forms.ModelForm): | |||
password = ReadOnlyPasswordHashField() | |||
class Meta: | |||
model = LessPassUser | |||
fields = ('email', 'password', 'is_active', 'is_admin') | |||
def clean_password(self): | |||
return self.initial["password"] | |||
class LessPassUserAdmin(BaseUserAdmin): | |||
form = UserChangeForm | |||
add_form = UserCreationForm | |||
list_display = ('email', 'is_admin', 'column_passwords_count') | |||
list_filter = ('is_admin', 'is_active') | |||
fieldsets = ( | |||
(None, {'fields': ('email', 'password')}), | |||
('Permissions', {'fields': ('is_admin',)}), | |||
) | |||
add_fieldsets = ( | |||
(None, { | |||
'classes': ('wide',), | |||
'fields': ('email', 'password1', 'password2')} | |||
), | |||
) | |||
search_fields = ('email',) | |||
ordering = ('email',) | |||
filter_horizontal = () | |||
def get_queryset(self, request): | |||
return models.LessPassUser.objects.annotate(passwords_count=Count('passwords')) | |||
def column_passwords_count(self, instance): | |||
return instance.passwords_count | |||
column_passwords_count.short_description = 'Password count' | |||
column_passwords_count.admin_order_field = 'passwords_count' | |||
class PasswordAdmin(admin.ModelAdmin): | |||
list_display = ('id', 'user',) | |||
search_fields = ('user__email',) | |||
ordering = ('user',) | |||
admin.site.register(models.Password, PasswordAdmin) | |||
admin.site.register(LessPassUser, LessPassUserAdmin) | |||
admin.site.unregister(Group) |
@@ -0,0 +1,5 @@ | |||
from django.apps import AppConfig | |||
class ApiConfig(AppConfig): | |||
name = 'api' |
@@ -0,0 +1,73 @@ | |||
# -*- coding: utf-8 -*- | |||
# Generated by Django 1.9.5 on 2016-04-06 10:11 | |||
from __future__ import unicode_literals | |||
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 = [ | |||
] | |||
operations = [ | |||
migrations.CreateModel( | |||
name='LessPassUser', | |||
fields=[ | |||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | |||
('password', models.CharField(max_length=128, verbose_name='password')), | |||
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), | |||
('email', models.EmailField(max_length=255, unique=True, verbose_name='email address')), | |||
('is_active', models.BooleanField(default=True)), | |||
('is_admin', models.BooleanField(default=False)), | |||
], | |||
options={ | |||
'verbose_name_plural': 'Users', | |||
}, | |||
), | |||
migrations.CreateModel( | |||
name='Entry', | |||
fields=[ | |||
('created', models.DateTimeField(auto_now_add=True, verbose_name='created')), | |||
('modified', models.DateTimeField(auto_now=True, verbose_name='modified')), | |||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), | |||
('login', models.CharField(default='', max_length=255)), | |||
('site', models.CharField(default='', max_length=255)), | |||
('title', models.CharField(blank=True, max_length=255, null=True)), | |||
('username', models.CharField(blank=True, max_length=255, null=True)), | |||
('email', models.EmailField(blank=True, max_length=254, null=True)), | |||
('description', models.TextField(blank=True, null=True)), | |||
('url', models.URLField(blank=True, null=True)), | |||
], | |||
options={ | |||
'verbose_name_plural': 'Entries', | |||
}, | |||
), | |||
migrations.CreateModel( | |||
name='PasswordInfo', | |||
fields=[ | |||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), | |||
('counter', models.IntegerField(default=1)), | |||
('settings', models.TextField()), | |||
('length', models.IntegerField(default=12)), | |||
], | |||
options={ | |||
'verbose_name_plural': 'Password info', | |||
}, | |||
), | |||
migrations.AddField( | |||
model_name='entry', | |||
name='password', | |||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.PasswordInfo'), | |||
), | |||
migrations.AddField( | |||
model_name='entry', | |||
name='user', | |||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='entries', to=settings.AUTH_USER_MODEL), | |||
), | |||
] |
@@ -0,0 +1,38 @@ | |||
# -*- coding: utf-8 -*- | |||
# Generated by Django 1.9.6 on 2016-09-24 08:11 | |||
from __future__ import unicode_literals | |||
from django.conf import settings | |||
from django.db import migrations, models | |||
import django.db.models.deletion | |||
import uuid | |||
class Migration(migrations.Migration): | |||
dependencies = [ | |||
('api', '0001_initial'), | |||
] | |||
operations = [ | |||
migrations.CreateModel( | |||
name='Password', | |||
fields=[ | |||
('created', models.DateTimeField(auto_now_add=True, verbose_name='created')), | |||
('modified', models.DateTimeField(auto_now=True, verbose_name='modified')), | |||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), | |||
('login', models.CharField(blank=True, max_length=255, null=True)), | |||
('site', models.CharField(blank=True, max_length=255, null=True)), | |||
('lowercase', models.BooleanField(default=True)), | |||
('uppercase', models.BooleanField(default=True)), | |||
('symbols', models.BooleanField(default=True)), | |||
('numbers', models.BooleanField(default=True)), | |||
('counter', models.IntegerField(default=1)), | |||
('length', models.IntegerField(default=12)), | |||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='passwords', to=settings.AUTH_USER_MODEL)), | |||
], | |||
options={ | |||
'abstract': False, | |||
}, | |||
), | |||
] |
@@ -0,0 +1,37 @@ | |||
# -*- coding: utf-8 -*- | |||
# Generated by Django 1.9.6 on 2016-09-24 08:13 | |||
from __future__ import unicode_literals | |||
from django.db import migrations | |||
import json | |||
from api import models | |||
def create_password_with(entry): | |||
settings = json.dumps(entry.password.settings) | |||
lowercase = 'lowercase' in settings | |||
uppercase = 'uppercase' in settings | |||
symbols = 'symbols' in settings | |||
numbers = 'numbers' in settings | |||
user = models.LessPassUser.objects.get(id=entry.user.id) | |||
models.Password.objects.create(id=entry.id, site=entry.site, login=entry.login, user=user, | |||
lowercase=lowercase, uppercase=uppercase, symbols=symbols, numbers=numbers, | |||
counter=entry.password.counter, length=entry.password.length) | |||
def mv_entries_to_password(apps, schema_editor): | |||
Entry = apps.get_model("api", "Entry") | |||
for entry in Entry.objects.all(): | |||
create_password_with(entry) | |||
class Migration(migrations.Migration): | |||
dependencies = [ | |||
('api', '0002_password'), | |||
] | |||
operations = [ | |||
migrations.RunPython(mv_entries_to_password), | |||
] |
@@ -0,0 +1,29 @@ | |||
# -*- coding: utf-8 -*- | |||
# Generated by Django 1.10.1 on 2016-10-25 06:33 | |||
from __future__ import unicode_literals | |||
from django.db import migrations | |||
class Migration(migrations.Migration): | |||
dependencies = [ | |||
('api', '0003_mv_entries_to_password'), | |||
] | |||
operations = [ | |||
migrations.RemoveField( | |||
model_name='entry', | |||
name='password', | |||
), | |||
migrations.RemoveField( | |||
model_name='entry', | |||
name='user', | |||
), | |||
migrations.DeleteModel( | |||
name='Entry', | |||
), | |||
migrations.DeleteModel( | |||
name='PasswordInfo', | |||
), | |||
] |
@@ -0,0 +1,20 @@ | |||
# -*- coding: utf-8 -*- | |||
# Generated by Django 1.10.3 on 2016-11-21 13:30 | |||
from __future__ import unicode_literals | |||
from django.db import migrations, models | |||
class Migration(migrations.Migration): | |||
dependencies = [ | |||
('api', '0004_remove_entries_password_info_models'), | |||
] | |||
operations = [ | |||
migrations.AddField( | |||
model_name='password', | |||
name='version', | |||
field=models.IntegerField(default=1), | |||
), | |||
] |
@@ -0,0 +1,25 @@ | |||
# -*- coding: utf-8 -*- | |||
# Generated by Django 1.11 on 2017-05-19 18:03 | |||
from __future__ import unicode_literals | |||
from django.db import migrations, models | |||
class Migration(migrations.Migration): | |||
dependencies = [ | |||
('api', '0005_password_version'), | |||
] | |||
operations = [ | |||
migrations.AlterField( | |||
model_name='password', | |||
name='length', | |||
field=models.IntegerField(default=16), | |||
), | |||
migrations.AlterField( | |||
model_name='password', | |||
name='version', | |||
field=models.IntegerField(default=2), | |||
), | |||
] |
@@ -0,0 +1,90 @@ | |||
import uuid | |||
from django.db import models | |||
from django.contrib.auth.models import ( | |||
BaseUserManager, AbstractBaseUser | |||
) | |||
class LesspassUserManager(BaseUserManager): | |||
def create_user(self, email, password=None): | |||
if not email: | |||
raise ValueError('Users must have an email address') | |||
user = self.model( | |||
email=self.normalize_email(email), | |||
) | |||
user.set_password(password) | |||
user.save(using=self._db) | |||
return user | |||
def create_superuser(self, email, password): | |||
user = self.create_user(email, password=password, ) | |||
user.is_admin = True | |||
user.save(using=self._db) | |||
return user | |||
class LessPassUser(AbstractBaseUser): | |||
email = models.EmailField(verbose_name='email address', max_length=255, unique=True) | |||
is_active = models.BooleanField(default=True) | |||
is_admin = models.BooleanField(default=False) | |||
objects = LesspassUserManager() | |||
USERNAME_FIELD = 'email' | |||
def get_full_name(self): | |||
return self.email | |||
def get_short_name(self): | |||
return self.email | |||
def __str__(self): | |||
return self.email | |||
def has_perm(self, perm, obj=None): | |||
return True | |||
def has_module_perms(self, app_label): | |||
return True | |||
@property | |||
def is_superuser(self): | |||
return self.is_admin | |||
@property | |||
def is_staff(self): | |||
return self.is_admin | |||
class Meta: | |||
verbose_name_plural = "Users" | |||
class DateMixin(models.Model): | |||
created = models.DateTimeField(auto_now_add=True, verbose_name='created') | |||
modified = models.DateTimeField(auto_now=True, verbose_name='modified') | |||
class Meta: | |||
abstract = True | |||
class Password(DateMixin): | |||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) | |||
user = models.ForeignKey(LessPassUser, on_delete=models.CASCADE, related_name='passwords') | |||
login = models.CharField(max_length=255, null=True, blank=True) | |||
site = models.CharField(max_length=255, null=True, blank=True) | |||
lowercase = models.BooleanField(default=True) | |||
uppercase = models.BooleanField(default=True) | |||
symbols = models.BooleanField(default=True) | |||
numbers = models.BooleanField(default=True) | |||
length = models.IntegerField(default=16) | |||
counter = models.IntegerField(default=1) | |||
version = models.IntegerField(default=2) | |||
def __str__(self): | |||
return str(self.id) | |||
@@ -0,0 +1,6 @@ | |||
from rest_framework import permissions | |||
class IsOwner(permissions.BasePermission): | |||
def has_object_permission(self, request, view, obj): | |||
return obj.user == request.user |
@@ -0,0 +1,14 @@ | |||
from api import models | |||
from rest_framework import serializers | |||
class PasswordSerializer(serializers.ModelSerializer): | |||
class Meta: | |||
model = models.Password | |||
fields = ('id', 'login', 'site', 'lowercase', 'uppercase', 'symbols', 'numbers', 'counter', 'length', | |||
'version', 'created', 'modified') | |||
read_only_fields = ('created', 'modified') | |||
def create(self, validated_data): | |||
user = self.context['request'].user | |||
return models.Password.objects.create(user=user, **validated_data) |
@@ -0,0 +1,25 @@ | |||
import factory | |||
from api import models | |||
class UserFactory(factory.DjangoModelFactory): | |||
class Meta: | |||
model = models.LessPassUser | |||
email = factory.Sequence(lambda n: 'u{0}@lesspass.com'.format(n)) | |||
password = factory.PostGenerationMethodCall('set_password', 'password') | |||
is_admin = False | |||
class AdminFactory(UserFactory): | |||
is_admin = True | |||
class PasswordFactory(factory.DjangoModelFactory): | |||
class Meta: | |||
model = models.Password | |||
user = factory.SubFactory(UserFactory) | |||
login = 'admin@oslab.fr' | |||
site = 'lesspass.com' |
@@ -0,0 +1,104 @@ | |||
from rest_framework.test import APITestCase, APIClient | |||
from api import models | |||
from api.tests import factories | |||
class LogoutApiTestCase(APITestCase): | |||
def test_get_passwords_401(self): | |||
response = self.client.get('/api/passwords/') | |||
self.assertEqual(401, response.status_code) | |||
class LoginApiTestCase(APITestCase): | |||
def setUp(self): | |||
self.user = factories.UserFactory() | |||
self.client = APIClient() | |||
self.client.force_authenticate(user=self.user) | |||
def test_get_empty_passwords(self): | |||
request = self.client.get('/api/passwords/') | |||
self.assertEqual(0, len(request.data['results'])) | |||
def test_retrieve_its_own_passwords(self): | |||
password = factories.PasswordFactory(user=self.user) | |||
request = self.client.get('/api/passwords/') | |||
self.assertEqual(1, len(request.data['results'])) | |||
self.assertEqual(password.site, request.data['results'][0]['site']) | |||
def test_cant_retrieve_other_passwords(self): | |||
not_my_password = factories.PasswordFactory(user=factories.UserFactory()) | |||
request = self.client.get('/api/passwords/%s/' % not_my_password.id) | |||
self.assertEqual(404, request.status_code) | |||
def test_delete_its_own_passwords(self): | |||
password = factories.PasswordFactory(user=self.user) | |||
self.assertEqual(1, models.Password.objects.all().count()) | |||
request = self.client.delete('/api/passwords/%s/' % password.id) | |||
self.assertEqual(204, request.status_code) | |||
self.assertEqual(0, models.Password.objects.all().count()) | |||
def test_cant_delete_other_password(self): | |||
not_my_password = factories.PasswordFactory(user=factories.UserFactory()) | |||
self.assertEqual(1, models.Password.objects.all().count()) | |||
request = self.client.delete('/api/passwords/%s/' % not_my_password.id) | |||
self.assertEqual(404, request.status_code) | |||
self.assertEqual(1, models.Password.objects.all().count()) | |||
def test_create_password(self): | |||
password = { | |||
"site": "lesspass.com", | |||
"login": "test@oslab.fr", | |||
"lowercase": True, | |||
"uppercase": True, | |||
"number": True, | |||
"symbol": True, | |||
"counter": 1, | |||
"length": 12 | |||
} | |||
self.assertEqual(0, models.Password.objects.count()) | |||
self.client.post('/api/passwords/', password) | |||
self.assertEqual(1, models.Password.objects.count()) | |||
def test_create_password_v2(self): | |||
password = { | |||
"site": "lesspass.com", | |||
"login": "test@oslab.fr", | |||
"lowercase": True, | |||
"uppercase": True, | |||
"number": True, | |||
"symbol": True, | |||
"counter": 1, | |||
"length": 12, | |||
"version": 2 | |||
} | |||
self.client.post('/api/passwords/', password) | |||
self.assertEqual(2, models.Password.objects.first().version) | |||
def test_update_password(self): | |||
password = factories.PasswordFactory(user=self.user) | |||
self.assertNotEqual('facebook.com', password.site) | |||
new_password = { | |||
"site": "facebook.com", | |||
"login": "test@oslab.fr", | |||
"lowercase": True, | |||
"uppercase": True, | |||
"number": True, | |||
"symbol": True, | |||
"counter": 1, | |||
"length": 12 | |||
} | |||
request = self.client.put('/api/passwords/%s/' % password.id, new_password) | |||
self.assertEqual(200, request.status_code, request.content.decode('utf-8')) | |||
password_updated = models.Password.objects.get(id=password.id) | |||
self.assertEqual('facebook.com', password_updated.site) | |||
def test_cant_update_other_password(self): | |||
not_my_password = factories.PasswordFactory(user=factories.UserFactory()) | |||
self.assertEqual('lesspass.com', not_my_password.site) | |||
new_password = { | |||
"site": "facebook", | |||
} | |||
request = self.client.put('/api/passwords/%s/' % not_my_password.id, new_password) | |||
self.assertEqual(404, request.status_code) | |||
self.assertEqual(1, models.Password.objects.all().count()) |
@@ -0,0 +1,15 @@ | |||
import rest_framework_jwt.views | |||
from django.conf.urls import url, include | |||
from rest_framework.routers import DefaultRouter | |||
from api import views | |||
router = DefaultRouter() | |||
router.register(r'passwords', views.PasswordViewSet, base_name='passwords') | |||
urlpatterns = [ | |||
url(r'^', include(router.urls)), | |||
url(r'^tokens/auth/', rest_framework_jwt.views.obtain_jwt_token), | |||
url(r'^tokens/refresh/', rest_framework_jwt.views.refresh_jwt_token), | |||
url(r'^auth/', include('djoser.urls')), | |||
] |
@@ -0,0 +1,14 @@ | |||
from api import models, serializers | |||
from api.permissions import IsOwner | |||
from rest_framework import permissions, viewsets | |||
class PasswordViewSet(viewsets.ModelViewSet): | |||
serializer_class = serializers.PasswordSerializer | |||
permission_classes = (permissions.IsAuthenticated, IsOwner,) | |||
search_fields = ('site', 'email',) | |||
ordering_fields = ('site', 'email', 'created') | |||
def get_queryset(self): | |||
return models.Password.objects.filter(user=self.request.user) |
@@ -0,0 +1,8 @@ | |||
#!/bin/sh | |||
while ! nc -z db 5432; do sleep 3; done | |||
python manage.py migrate | |||
python manage.py collectstatic --clear --no-input | |||
exec "$@" |
@@ -0,0 +1,193 @@ | |||
import logging | |||
import os | |||
import random | |||
import sys | |||
import datetime | |||
from envparse import env | |||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | |||
def get_secret_key(secret_key): | |||
if not secret_key: | |||
return "".join([random.choice("abcdefghijklmnopqrstuvwxyz0123456789!@#$^&*(-_=+)") for i in range(50)]) | |||
return secret_key | |||
SECRET_KEY = env('SECRET_KEY', preprocessor=get_secret_key, default=None) | |||
DEBUG = env.bool('DJANGO_DEBUG', default=False) | |||
ALLOWED_HOSTS = env('ALLOWED_HOSTS', cast=list, default=['localhost', '127.0.0.1', '.lesspass.com']) | |||
ADMINS = (('Guillaume Vincent', 'guillaume@oslab.fr'),) | |||
INSTALLED_APPS = [ | |||
'django.contrib.admin', | |||
'django.contrib.auth', | |||
'django.contrib.contenttypes', | |||
'django.contrib.sessions', | |||
'django.contrib.messages', | |||
'django.contrib.staticfiles', | |||
'rest_framework', | |||
'corsheaders', | |||
'djoser', | |||
'api' | |||
] | |||
MIDDLEWARE = [ | |||
'django.middleware.security.SecurityMiddleware', | |||
'django.contrib.sessions.middleware.SessionMiddleware', | |||
'corsheaders.middleware.CorsMiddleware', | |||
'django.middleware.common.CommonMiddleware', | |||
'django.middleware.csrf.CsrfViewMiddleware', | |||
'django.contrib.auth.middleware.AuthenticationMiddleware', | |||
'django.contrib.messages.middleware.MessageMiddleware', | |||
'django.middleware.clickjacking.XFrameOptionsMiddleware', | |||
] | |||
CORS_ORIGIN_ALLOW_ALL = True | |||
ROOT_URLCONF = 'lesspass.urls' | |||
TEMPLATES = [ | |||
{ | |||
'BACKEND': 'django.template.backends.django.DjangoTemplates', | |||
'DIRS': [], | |||
'APP_DIRS': True, | |||
'OPTIONS': { | |||
'context_processors': [ | |||
'django.template.context_processors.debug', | |||
'django.template.context_processors.request', | |||
'django.contrib.auth.context_processors.auth', | |||
'django.contrib.messages.context_processors.messages', | |||
], | |||
}, | |||
}, | |||
] | |||
WSGI_APPLICATION = 'lesspass.wsgi.application' | |||
DATABASES = { | |||
'default': { | |||
'ENGINE': env('DATABASE_ENGINE', default='django.db.backends.sqlite3'), | |||
'NAME': env('DATABASE_NAME', default=os.path.join(BASE_DIR, 'db.sqlite3')), | |||
'USER': env('DATABASE_USER', default=None), | |||
'PASSWORD': env('DATABASE_PASSWORD', default=None), | |||
'HOST': env('DATABASE_HOST', default=None), | |||
'PORT': env('DATABASE_PORT', default=None), | |||
} | |||
} | |||
AUTH_PASSWORD_VALIDATORS = [ | |||
{'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', }, | |||
{'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', }, | |||
{'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', }, | |||
{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', }, | |||
] | |||
LANGUAGE_CODE = 'en-us' | |||
TIME_ZONE = 'UTC' | |||
USE_I18N = True | |||
USE_L10N = True | |||
USE_TZ = True | |||
STATIC_URL = '/static/' | |||
STATIC_ROOT = os.path.join(BASE_DIR, 'www', 'static') | |||
MEDIA_URL = '/media/' | |||
MEDIA_ROOT = os.path.join(BASE_DIR, 'www', 'media') | |||
LOGGING = { | |||
'version': 1, | |||
'disable_existing_loggers': False, | |||
'handlers': { | |||
'console': { | |||
'class': 'logging.StreamHandler', | |||
} | |||
}, | |||
'root': { | |||
'handlers': ['console'], | |||
'level': env('DJANGO_LOG_LEVEL', default='DEBUG'), | |||
} | |||
} | |||
AUTH_USER_MODEL = 'api.LessPassUser' | |||
JWT_AUTH = { | |||
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7), | |||
'JWT_ALLOW_REFRESH': True, | |||
} | |||
REST_FRAMEWORK = { | |||
'DEFAULT_PERMISSION_CLASSES': ( | |||
'rest_framework.permissions.IsAuthenticated', | |||
), | |||
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', | |||
'PAGE_SIZE': 1000, | |||
'DEFAULT_FILTER_BACKENDS': ( | |||
'rest_framework.filters.OrderingFilter', | |||
'rest_framework.filters.SearchFilter', | |||
), | |||
'DEFAULT_AUTHENTICATION_CLASSES': ( | |||
'rest_framework_jwt.authentication.JSONWebTokenAuthentication', | |||
'rest_framework.authentication.BasicAuthentication', | |||
'rest_framework.authentication.SessionAuthentication', | |||
), | |||
'DEFAULT_RENDERER_CLASSES': ( | |||
'rest_framework.renderers.JSONRenderer', | |||
'rest_framework.renderers.BrowsableAPIRenderer', | |||
), | |||
'DEFAULT_PARSER_CLASSES': ( | |||
'rest_framework.parsers.JSONParser', | |||
), | |||
'TEST_REQUEST_DEFAULT_FORMAT': 'json' | |||
} | |||
class DisableMigrations(object): | |||
def __contains__(self, item): | |||
return True | |||
def __getitem__(self, item): | |||
return None | |||
TESTS_IN_PROGRESS = False | |||
if 'test' in sys.argv[1:] or 'jenkins' in sys.argv[1:]: | |||
logging.disable(logging.CRITICAL) | |||
PASSWORD_HASHERS = ( | |||
'django.contrib.auth.hashers.MD5PasswordHasher', | |||
) | |||
DEBUG = False | |||
TEMPLATE_DEBUG = False | |||
TESTS_IN_PROGRESS = True | |||
MIGRATION_MODULES = DisableMigrations() | |||
DJOSER = { | |||
'PASSWORD_RESET_CONFIRM_URL': '#/password/reset/confirm/{uid}/{token}', | |||
'ACTIVATION_URL': '#/activate/{uid}/{token}' | |||
} | |||
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') | |||
SESSION_COOKIE_SECURE = True | |||
CSRF_COOKIE_SECURE = True | |||
if DEBUG: | |||
EMAIL_BACKEND = os.getenv('EMAIL_BACKEND', 'django.core.mail.backends.console.EmailBackend') | |||
else: | |||
EMAIL_BACKEND = os.getenv('EMAIL_BACKEND', 'django.core.mail.backends.smtp.EmailBackend') | |||
DEFAULT_FROM_EMAIL = os.getenv('DEFAULT_FROM_EMAIL', 'contact@lesspass.com') | |||
EMAIL_HOST = os.getenv('EMAIL_HOST', 'localhost') | |||
EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER', '') | |||
EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD', '') | |||
EMAIL_PORT = env.int('EMAIL_PORT', default=25) | |||
EMAIL_SUBJECT_PREFIX = os.getenv('EMAIL_SUBJECT_PREFIX', '[LessPass] ') | |||
EMAIL_USE_TLS = env.bool('EMAIL_USE_TLS', default=False) | |||
EMAIL_USE_SSL = env.bool('EMAIL_USE_SSL', default=False) |
@@ -0,0 +1,7 @@ | |||
from django.conf.urls import include, url | |||
from django.contrib import admin | |||
urlpatterns = [ | |||
url(r'^api/', include('api.urls')), | |||
url(r'^admin/', admin.site.urls), | |||
] |
@@ -0,0 +1,7 @@ | |||
import os | |||
from django.core.wsgi import get_wsgi_application | |||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "lesspass.settings") | |||
application = get_wsgi_application() |
@@ -0,0 +1,10 @@ | |||
#!/usr/bin/env python | |||
import os | |||
import sys | |||
if __name__ == "__main__": | |||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "lesspass.settings") | |||
from django.core.management import execute_from_command_line | |||
execute_from_command_line(sys.argv) |
@@ -0,0 +1,10 @@ | |||
Django==1.11 | |||
django-cors-middleware==1.3.1 | |||
djangorestframework==3.6.2 | |||
djangorestframework-jwt==1.10.0 | |||
psycopg2==2.7.1 | |||
gunicorn==19.7.1 | |||
djoser==0.5.4 | |||
envparse==0.2.0 | |||
# tests | |||
factory-boy==2.8.1 |
@@ -0,0 +1,14 @@ | |||
[supervisord] | |||
nodaemon=true | |||
logfile=/dev/null | |||
pidfile=/var/run/supervisord.pid | |||
[program:gunicorn] | |||
directory=/backend | |||
command=gunicorn lesspass.wsgi:application -w 2 -b :8000 | |||
autostart=true | |||
autorestart=true | |||
stdout_logfile=/dev/stdout | |||
stdout_logfile_maxbytes=0 | |||
stderr_logfile=/dev/stderr | |||
stderr_logfile_maxbytes=0 |
@@ -1 +0,0 @@ | |||
Subproject commit d41a7867ffd23cb76c14a4505d72b1830b0e5e4e |
@@ -0,0 +1,17 @@ | |||
# editorconfig.org | |||
root = true | |||
[*] | |||
charset = utf-8 | |||
end_of_line = lf | |||
indent_size = 2 | |||
indent_style = space | |||
insert_final_newline = true | |||
trim_trailing_whitespace = true | |||
[*.md] | |||
trim_trailing_whitespace = false | |||
[*.py] | |||
indent_size = 4 |
@@ -0,0 +1 @@ | |||
node_modules |
@@ -0,0 +1,13 @@ | |||
dist: trusty | |||
sudo: required | |||
language: node_js | |||
node_js: | |||
- '6' | |||
addons: | |||
apt: | |||
packages: | |||
- xsel | |||
before_script: | |||
- "export DISPLAY=:99.0" | |||
- "sh -e /etc/init.d/xvfb start" | |||
- sleep 3 |
@@ -0,0 +1,80 @@ | |||
# LessPass cli | |||
LessPass passwords directly in your terminal | |||
## Install | |||
``` | |||
$ npm install --global lesspass-cli | |||
``` | |||
## Usage | |||
``` | |||
$ lesspass --help | |||
build LessPass passwords directly in command line | |||
Usage | |||
$ lesspass <site> <login> [masterPassword] [options] | |||
Options | |||
-l add lowercase in password | |||
-u add uppercase in password | |||
-d add digits in password | |||
-s add symbols in password | |||
--no-lowercase remove lowercase from password | |||
--no-uppercase remove uppercase from password | |||
--no-digits remove digits from password | |||
--no-symbols remove symbols from password | |||
--length, -L int (default 16) | |||
--counter, -c int (default 1) | |||
--clipboard, -C copy generated password to clipboard rather than displaying it. | |||
Need pbcopy (OSX), xclip (Linux) or clip (Windows). | |||
Examples | |||
# no symbols | |||
$ lesspass lesspass.com contact@lesspass.com password --no-symbols | |||
OlfK63bmUhqrGODR | |||
# no symbols shortcut | |||
$ lesspass lesspass.com contact@lesspass.com password -lud | |||
OlfK63bmUhqrGODR | |||
# only digits and length of 8 | |||
$ lesspass lesspass.com contact@lesspass.com -d -L8 | |||
master password: | |||
75837019 | |||
``` | |||
## FAQ | |||
### How can I generate a password if I have a quote (`'`) in my master password ? | |||
Escape the quote like this : | |||
lesspass lesspass.com contact@lesspass.com 'my parents'\'' house is great' | |||
Replace `'` by `'\''` | |||
### password prompt | |||
If you omit master password, lesspass-cli will ask you a master password: | |||
lesspass lesspass.com contact@lesspass.com --length=14 | |||
master password: | |||
## License | |||
This project is licensed under the terms of the GNU GPLv3. | |||
## Issues | |||
report issues on [LessPass project](https://github.com/lesspass/lesspass/issues) |
@@ -0,0 +1,145 @@ | |||
#!/usr/bin/env node | |||
"use strict"; | |||
const clipboardy = require("clipboardy"); | |||
const meow = require("meow"); | |||
const LessPass = require("lesspass"); | |||
const read = require("read"); | |||
const chalk = require("chalk"); | |||
const helpMessage = ` | |||
Usage | |||
$ lesspass <site> <login> [masterPassword] [options] | |||
Options | |||
-l add lowercase in password | |||
-u add uppercase in password | |||
-d add digits in password | |||
-s add symbols in password | |||
--no-lowercase remove lowercase from password | |||
--no-uppercase remove uppercase from password | |||
--no-digits remove digits from password | |||
--no-symbols remove symbols from password | |||
--length, -L int (default 16) | |||
--counter, -c int (default 1) | |||
--clipboard, -C copy generated password to clipboard rather than displaying it. | |||
Need pbcopy (OSX), xsel (Linux) or clip (Windows). | |||
Examples | |||
# no symbols | |||
$ lesspass lesspass.com contact@lesspass.com password --no-symbols | |||
OlfK63bmUhqrGODR | |||
# no symbols shortcut | |||
$ lesspass lesspass.com contact@lesspass.com password -lud | |||
OlfK63bmUhqrGODR | |||
# only digits and length of 8 | |||
$ lesspass lesspass.com contact@lesspass.com -d -L8 | |||
master password: | |||
75837019`; | |||
const cli = meow(helpMessage, { | |||
flags: { | |||
site: { type: "string" }, | |||
login: { type: "string" }, | |||
length: { | |||
type: "string", | |||
alias: "L" | |||
}, | |||
counter: { | |||
type: "string", | |||
alias: "c" | |||
}, | |||
clipboard: { | |||
type: "boolean", | |||
alias: "C" | |||
}, | |||
l: { type: "boolean" }, | |||
u: { type: "boolean" }, | |||
d: { type: "boolean" }, | |||
s: { type: "boolean" } | |||
} | |||
}); | |||
function calcPassword(site, login, masterPassword, passwordProfile) { | |||
LessPass.generatePassword(site, login, masterPassword, passwordProfile).then( | |||
function(generatedPassword) { | |||
if (passwordProfile.clipboard) { | |||
clipboardy | |||
.write(generatedPassword) | |||
.then(() => { | |||
console.log("Copied to clipboard"); | |||
process.exit(); | |||
}) | |||
.catch(err => { | |||
console.error(chalk.red("Copy failed.")); | |||
console.error(err.message); | |||
process.exit(1); | |||
}); | |||
} else { | |||
console.log(generatedPassword); | |||
process.exit(); | |||
} | |||
} | |||
); | |||
} | |||
function hasNoShortOption(options) { | |||
return !["l", "u", "d", "s"].some(function(shortOption) { | |||
return typeof options[shortOption] !== "undefined" && options[shortOption]; | |||
}); | |||
} | |||
function getOptionBoolean(options, optionString) { | |||
let shortOption = optionString.substring(0, 1); | |||
if (options[shortOption]) { | |||
return true; | |||
} | |||
if (typeof options[optionString] === "undefined") { | |||
return hasNoShortOption(options); | |||
} | |||
return options[optionString]; | |||
} | |||
const lowercase = getOptionBoolean(cli.flags, "lowercase"); | |||
const uppercase = getOptionBoolean(cli.flags, "uppercase"); | |||
const symbols = getOptionBoolean(cli.flags, "symbols"); | |||
const digits = getOptionBoolean(cli.flags, "digits"); | |||
const passwordProfile = { | |||
lowercase: lowercase, | |||
uppercase: uppercase, | |||
symbols: symbols, | |||
numbers: digits, | |||
clipboard: cli.flags.clipboard || false, | |||
length: cli.flags.length || 16, | |||
counter: cli.flags.counter || 1 | |||
}; | |||
const site = cli.input[0]; | |||
let login = cli.input[1]; | |||
if (typeof login === "undefined") { | |||
login = ""; | |||
} | |||
if (typeof site === "undefined") { | |||
console.log(chalk.red("site cannot be empty")); | |||
console.log("type lesspass --help"); | |||
process.exit(-1); | |||
} | |||
if (cli.input.length === 3) { | |||
const masterPassword = cli.input[2]; | |||
calcPassword(site, login, masterPassword, passwordProfile); | |||
} else { | |||
read({ prompt: "master password: ", silent: true }, function(er, password) { | |||
if (er && er.message === "canceled") { | |||
process.exit(); | |||
} | |||
calcPassword(site, login, password, passwordProfile); | |||
}); | |||
} |
@@ -0,0 +1,37 @@ | |||
{ | |||
"name": "lesspass-cli", | |||
"version": "5.1.1", | |||
"description": "build LessPass passwords directly in command line", | |||
"keywords": [ | |||
"cli", | |||
"cli-app", | |||
"lesspass", | |||
"password" | |||
], | |||
"license": "GPL-3.0", | |||
"author": "Guillaume Vincent <guillaume@oslab.fr>", | |||
"files": [ | |||
"cli.js" | |||
], | |||
"bin": { | |||
"lesspass": "cli.js" | |||
}, | |||
"repository": "lesspass/cli", | |||
"scripts": { | |||
"precommit": "npm test", | |||
"prepush": "npm test", | |||
"test": "ava" | |||
}, | |||
"dependencies": { | |||
"chalk": "2.3.1", | |||
"clipboardy": "1.2.3", | |||
"lesspass": "6.0.0", | |||
"meow": "4.0.0", | |||
"read": "1.0.7" | |||
}, | |||
"devDependencies": { | |||
"ava": "^0.25.0", | |||
"execa": "^0.9.0", | |||
"husky": "^0.14.3" | |||
} | |||
} |
@@ -0,0 +1,292 @@ | |||
import test from "ava"; | |||
import execa from "execa"; | |||
test("default options", async t => { | |||
const { stdout } = await execa("./cli.js", [ | |||
"lesspass.com", | |||
"contact@lesspass.com", | |||
"password" | |||
]); | |||
t.is(stdout, "\\g-A1-.OHEwrXjT#"); | |||
}); | |||
test("no login", async t => { | |||
return execa.shell('echo password | ./cli.js "lesspass.com"').then(result => { | |||
t.is(result.stdout, "master password: 7Cw-APO5Co?G>W>u"); | |||
}); | |||
}); | |||
test("options can be before parameters", async t => { | |||
const { stdout } = await execa("./cli.js", [ | |||
"-C", | |||
"lesspass.com", | |||
"contact@lesspass.com", | |||
"password" | |||
]); | |||
t.is(stdout, "Copied to clipboard"); | |||
}); | |||
test("long options can be before parameters", async t => { | |||
const { stdout } = await execa("./cli.js", [ | |||
"--clipboard", | |||
"lesspass.com", | |||
"contact@lesspass.com", | |||
"password" | |||
]); | |||
t.is(stdout, "Copied to clipboard"); | |||
}); | |||
test("length", async t => { | |||
const { stdout } = await execa("./cli.js", [ | |||
"lesspass.com", | |||
"contact@lesspass.com", | |||
"password", | |||
"--length=14" | |||
]); | |||
t.is(stdout, "=0\\A-.OHEKvwrX"); | |||
}); | |||
test("length shortcut", async t => { | |||
const { stdout } = await execa("./cli.js", [ | |||
"lesspass.com", | |||
"contact@lesspass.com", | |||
"password", | |||
"-L=14" | |||
]); | |||
t.is(stdout, "=0\\A-.OHEKvwrX"); | |||
}); | |||
test("counter", async t => { | |||
const { stdout } = await execa("./cli.js", [ | |||
"lesspass.com", | |||
"contact@lesspass.com", | |||
"password", | |||
"--counter=2" | |||
]); | |||
t.is(stdout, "Vf:F1'!I`8Y2`GBE"); | |||
}); | |||
test("counter shortcut", async t => { | |||
const { stdout } = await execa("./cli.js", [ | |||
"lesspass.com", | |||
"contact@lesspass.com", | |||
"password", | |||
"-c=2" | |||
]); | |||
t.is(stdout, "Vf:F1'!I`8Y2`GBE"); | |||
}); | |||
test("no lowercase", async t => { | |||
const { stdout } = await execa("./cli.js", [ | |||
"lesspass.com", | |||
"contact@lesspass.com", | |||
"password", | |||
"--no-lowercase" | |||
]); | |||
t.is(stdout, 'JBG\\`3{+0["(E\\JJ'); | |||
}); | |||
test("no lowercase shortcut", async t => { | |||
const { stdout } = await execa("./cli.js", [ | |||
"lesspass.com", | |||
"contact@lesspass.com", | |||
"password", | |||
"-uds" | |||
]); | |||
t.is(stdout, 'JBG\\`3{+0["(E\\JJ'); | |||
}); | |||
test("only lowercase", async t => { | |||
const { stdout } = await execa("./cli.js", [ | |||
"lesspass.com", | |||
"contact@lesspass.com", | |||
"password", | |||
"-l" | |||
]); | |||
t.is(stdout, "fmnujoqgcxmpffyh"); | |||
}); | |||
test("no uppercase", async t => { | |||
const { stdout } = await execa("./cli.js", [ | |||
"lesspass.com", | |||
"contact@lesspass.com", | |||
"password", | |||
"--no-uppercase" | |||
]); | |||
t.is(stdout, 'jbg\\`3{+0["(e\\jj'); | |||
}); | |||
test("no uppercase shortcut", async t => { | |||
const { stdout } = await execa("./cli.js", [ | |||
"lesspass.com", | |||
"contact@lesspass.com", | |||
"password", | |||
"-lds" | |||
]); | |||
t.is(stdout, 'jbg\\`3{+0["(e\\jj'); | |||
}); | |||
test("only uppercase", async t => { | |||
const { stdout } = await execa("./cli.js", [ | |||
"lesspass.com", | |||
"contact@lesspass.com", | |||
"password", | |||
"-u" | |||
]); | |||
t.is(stdout, "FMNUJOQGCXMPFFYH"); | |||
}); | |||
test("no digits", async t => { | |||
const { stdout } = await execa("./cli.js", [ | |||
"lesspass.com", | |||
"contact@lesspass.com", | |||
"password", | |||
"--no-digits" | |||
]); | |||
t.is(stdout, ";zkB#m]mNF$;J_Ej"); | |||
}); | |||
test("no digits shortcut", async t => { | |||
const { stdout } = await execa("./cli.js", [ | |||
"lesspass.com", | |||
"contact@lesspass.com", | |||
"password", | |||
"-lus" | |||
]); | |||
t.is(stdout, ";zkB#m]mNF$;J_Ej"); | |||
}); | |||
test("only digits", async t => { | |||
const { stdout } = await execa("./cli.js", [ | |||
"lesspass.com", | |||
"contact@lesspass.com", | |||
"password", | |||
"-d" | |||
]); | |||
t.is(stdout, "7587019305478072"); | |||
}); | |||
test("no symbols", async t => { | |||
const { stdout } = await execa("./cli.js", [ | |||
"lesspass.com", | |||
"contact@lesspass.com", | |||
"password", | |||
"--no-symbols" | |||
]); | |||
t.is(stdout, "OlfK63bmUhqrGODR"); | |||
}); | |||
test("no symbols shortcut", async t => { | |||
const { stdout } = await execa("./cli.js", [ | |||
"lesspass.com", | |||
"contact@lesspass.com", | |||
"password", | |||
"-lud" | |||
]); | |||
t.is(stdout, "OlfK63bmUhqrGODR"); | |||
}); | |||
test("only symbols", async t => { | |||
const { stdout } = await execa("./cli.js", [ | |||
"lesspass.com", | |||
"contact@lesspass.com", | |||
"password", | |||
"-s" | |||
]); | |||
t.is(stdout, "<\"]|'`%};'`>-'[,"); | |||
}); | |||
test("test space in password", async t => { | |||
const { stdout } = await execa("./cli.js", [ | |||
"lesspass.com", | |||
"contact@lesspass.com", | |||
"my Master Password" | |||
]); | |||
t.is(stdout, "D1PBB34\\#fh!LY={"); | |||
}); | |||
test("doc 1", async t => { | |||
const { stdout } = await execa("./cli.js", [ | |||
"lesspass.com", | |||
"contact@lesspass.com", | |||
"password", | |||
"--no-symbols" | |||
]); | |||
t.is(stdout, "OlfK63bmUhqrGODR"); | |||
}); | |||
test("doc 1 options before", async t => { | |||
const { stdout } = await execa("./cli.js", [ | |||
"--no-symbols", | |||
"lesspass.com", | |||
"contact@lesspass.com", | |||
"password" | |||
]); | |||
t.is(stdout, "OlfK63bmUhqrGODR"); | |||
}); | |||
test("doc 2", async t => { | |||
const { stdout } = await execa("./cli.js", [ | |||
"lesspass.com", | |||
"contact@lesspass.com", | |||
"password", | |||
"-lud" | |||
]); | |||
t.is(stdout, "OlfK63bmUhqrGODR"); | |||
}); | |||
test("doc 2 options before", async t => { | |||
const { stdout } = await execa("./cli.js", [ | |||
"-lud", | |||
"lesspass.com", | |||
"contact@lesspass.com", | |||
"password" | |||
]); | |||
t.is(stdout, "OlfK63bmUhqrGODR"); | |||
}); | |||
test("doc 3", async t => { | |||
const { stdout } = await execa("./cli.js", [ | |||
"lesspass.com", | |||
"contact@lesspass.com", | |||
"password", | |||
"-d", | |||
"-L8" | |||
]); | |||
t.is(stdout, "75837019"); | |||
}); | |||
test("doc 3 options before", async t => { | |||
const { stdout } = await execa("./cli.js", [ | |||
"-d", | |||
"-L8", | |||
"lesspass.com", | |||
"contact@lesspass.com", | |||
"password" | |||
]); | |||
t.is(stdout, "75837019"); | |||
}); | |||
test("doc 3 options before and after", async t => { | |||
const { stdout } = await execa("./cli.js", [ | |||
"-d", | |||
"lesspass.com", | |||
"contact@lesspass.com", | |||
"password", | |||
"-L8" | |||
]); | |||
t.is(stdout, "75837019"); | |||
}); | |||
test("nrt numbers should be considered as string not integers", async t => { | |||
const p = execa("./cli.js", ["example.org", "123", "password"]); | |||
const p2 = execa("./cli.js", ["example.org", "0123", "password"]); | |||
const p3 = execa("./cli.js", ["example.org", '"0123"', "password"]); | |||
const p4 = execa("./cli.js", ["example.org", "00123", "password"]); | |||
return Promise.all([p, p2, p3, p4]).then(v => { | |||
t.is(v[0].stdout, "sMb8}N&`J4wkF9q~"); | |||
t.is(v[1].stdout, "5,4SqhB2[=/h\\DZh"); | |||
t.is(v[2].stdout, "u0Fz)EOJ4i\\{{;a~"); | |||
t.is(v[3].stdout, '=}|O7hN0ZHdjQ{">'); | |||
}); | |||
}); |
@@ -1 +0,0 @@ | |||
Subproject commit be8f56640289947a0ee7bac7f0047532e71c7f1a |
@@ -0,0 +1,17 @@ | |||
# editorconfig.org | |||
root = true | |||
[*] | |||
charset = utf-8 | |||
end_of_line = lf | |||
indent_size = 2 | |||
indent_style = space | |||
insert_final_newline = true | |||
trim_trailing_whitespace = true | |||
[*.md] | |||
trim_trailing_whitespace = false | |||
[*.py] | |||
indent_size = 4 |
@@ -0,0 +1,4 @@ | |||
platforms | |||
plugins | |||
res | |||
node_modules |
@@ -0,0 +1,28 @@ | |||
# LessPass cordova | |||
LessPass Android application | |||
## Requirements | |||
* Cordova (`npm install -g cordova`) | |||
## Check everything ok | |||
cordova requirements | |||
## Test | |||
cordova run android | |||
## Build Release | |||
cordova build --release -- --keystore=~/Android/lesspass.keystore --storePassword=... --alias=lesspass --password=... | |||
## License | |||
This project is licensed under the terms of the GNU GPLv3. | |||
## Issues | |||
report issues on [LessPass project](https://github.com/lesspass/lesspass/issues) |
@@ -0,0 +1,6 @@ | |||
#!/usr/bin/env bash | |||
cp www/icons/mipmap-hdpi/icon.png platforms/android/res/mipmap-hdpi/icon.png | |||
cp www/icons/mipmap-ldpi/icon.png platforms/android/res/mipmap-ldpi/icon.png | |||
cp www/icons/mipmap-mdpi/icon.png platforms/android/res/mipmap-mdpi/icon.png | |||
cp www/icons/mipmap-xhdpi/icon.png platforms/android/res/mipmap-xhdpi/icon.png |
@@ -0,0 +1,28 @@ | |||
<?xml version='1.0' encoding='utf-8'?> | |||
<widget android-versionCode="20101" | |||
id="com.lesspass.android" | |||
version="2.1.1" | |||
xmlns="http://www.w3.org/ns/widgets" | |||
xmlns:cdv="http://cordova.apache.org/ns/1.0"> | |||
<name>LessPass</name> | |||
<description> | |||
Stateless Password Manager | |||
</description> | |||
<author email="contact@lesspass.com" href="https://lesspass.com"> | |||
LessPass Team | |||
</author> | |||
<content src="index.html"/> | |||
<access origin="*"/> | |||
<allow-intent href="https://*/*"/> | |||
<allow-navigation href="https://*/*"/> | |||
<platform name="android"> | |||
<preference name="android-minSdkVersion" value="21"/> | |||
<allow-intent href="market:*"/> | |||
<icon density="hdpi" src="www/icons/mipmap-hdpi/icon.png"/> | |||
<icon density="ldpi" src="www/icons/mipmap-ldpi/icon.png"/> | |||
<icon density="mdpi" src="www/icons/mipmap-mdpi/icon.png"/> | |||
<icon density="xhdpi" src="www/icons/mipmap-xhdpi/icon.png"/> | |||
</platform> | |||
<engine name="android" spec="^6.2.3"/> | |||
<plugin name="cordova-plugin-whitelist" spec="~1.3.2"/> | |||
</widget> |
@@ -0,0 +1,16 @@ | |||
'use strict'; | |||
const gulp = require('gulp'); | |||
gulp.task('lesspass', [], function() { | |||
return gulp.src(['node_modules/lesspass-pure/dist/**/*']) | |||
.pipe(gulp.dest('www/dist/')); | |||
}); | |||
gulp.task('build', [], function() { | |||
gulp.start('lesspass'); | |||
}); | |||
gulp.task('default', ['build'], function() { | |||
}); |
@@ -0,0 +1,31 @@ | |||
{ | |||
"name": "lesspass-cordova", | |||
"version": "1.0.0", | |||
"description": "LessPass cordova app", | |||
"license": "GPL-3.0", | |||
"author": "Guillaume Vincent <guillaume@oslab.fr>", | |||
"repository": "lesspass/cordova", | |||
"scripts": { | |||
"prebuild": "npm prune && npm install", | |||
"clean": "rm -rf plateforms/android && rm -rf www/dist/ && cordova clean", | |||
"build": "npm run clean && gulp && ./build.sh" | |||
}, | |||
"dependencies": { | |||
"cordova": "^7.0.1", | |||
"cordova-android": "^6.2.3", | |||
"cordova-plugin-whitelist": "~1.3.2", | |||
"lesspass-pure": "^4.6.2" | |||
}, | |||
"devDependencies": { | |||
"gulp": "^3.9.1" | |||
}, | |||
"private": true, | |||
"cordova": { | |||
"plugins": { | |||
"cordova-plugin-whitelist": {} | |||
}, | |||
"platforms": [ | |||
"android" | |||
] | |||
} | |||
} |
@@ -0,0 +1,47 @@ | |||
{ | |||
"Advanced options": "Erweiterte Optionen", | |||
"Copied": "Kopiert!", | |||
"Counter": "Zähler", | |||
"CounterFieldHelp": "Erhöhen Sie diesen Wert, um das zu erstellende Passwort zu ändern, ohne Ihr Masterpasswort zu ändern.", | |||
"CreatePassword": "Möchten Sie eines erstellen?", | |||
"DBNotRunning": "Ihre LessPass Datenbank läuft nicht", | |||
"DefaultOptionLocalStorage": "Wir benutzen den Cache Ihres Browsers, um Ihre Einstellungen als Standard lokal zu speichern. Jedesmal, wenn Sie die App öffnen, werden diese Einstellungen geladen.", | |||
"Email": "email", | |||
"EmailAlreadyExist": "Diese email-Adresse ist bereits registriert. Möchten Sie sich einloggen oder Ihr Passwort wiederherstellen?", | |||
"EmailInvalid": "Bitte geben Sie eine gültige email-Adresse ein", | |||
"EmailRequiredError": "Wir benötigen eine email-Adresse, um Ihr Konto zu finden.", | |||
"Encrypt my master password": "Verschlüssele mein Masterpasswort", | |||
"ForgotPassword": "Passwort vergessen?", | |||
"Generate": "Erstelle", | |||
"Length": "Länge", | |||
"LessPass Database Url": "LessPass Datenbank Url", | |||
"Login": "Login", | |||
"LoginFormInvalid": "LessPass URL, email-Adresse und Passwort sind obligatorisch", | |||
"LoginIncorrectError": "Die email-Adresse und das Passwort, die Sie eingegeben haben, entsprechen nicht unseren Daten. Bitte überprüfen Sie sie und versuchen es nochmal.", | |||
"Master Password": "Masterpasswort", | |||
"Next": "Nächste", | |||
"NoMatchFor": "Ups! Dafür gibt es keine Übereinstimmung", | |||
"NoPassword": "Sie haben kein Passwort-Profil in Ihrer Datenbank gespeichert.", | |||
"Password profile deleted": "Kennwort Profil gelöscht", | |||
"PasswordProfileCopied": "Ihr Passwort-Profil wurde kopiert", | |||
"PasswordResetRequired": "Ein Passwort ist erforderlich", | |||
"PasswordResetSuccessful": "Ihr Passwort wurde erfolgreich zurückgesetzt.", | |||
"Previous": "Vorherige", | |||
"Register": "Registrieren", | |||
"Reset my password": "Mein Passwort zurücksetzen", | |||
"ResetLinkExpired": "Der Link zum Zurücksetzen des Passworts ist abgelaufen.", | |||
"Save options": "Optionen speichern", | |||
"Sign In": "Anmelden", | |||
"Site": "Seite", | |||
"SiteLoginMasterPasswordMandatory": "Die Felder für Seite, Anmeldename und Masterpasswort sind obligatorisch.", | |||
"SorryCopy": "Es tut uns leid, dass die Kopie nur auf modernen Browsern funktioniert", | |||
"UNDO": "STORNIEREN", | |||
"UpdateYourSearch": "Bitte erweitern Sie Ihre Suche.", | |||
"Version": "Version", | |||
"WarningV1Deprecated": "Version 1 ist veraltet und wird bald gelöscht werden. Wir empfehlen Ihnen dringend, Ihre Passwörter auf die Version 2 zu migrieren.", | |||
"WelcomeRegister": "Willkommen {email}, danke für die Anmeldung.", | |||
"Your options have been saved successfully": "Ihre Optionen wurden erfolgreich gespeichert", | |||
"resetPasswordSuccess": "Wenn die E-Mail-Adresse {email} mit einem LessPass-Konto verknüpft ist, erhalten Sie in Kürze eine E-Mail von LessPass mit Anweisungen zum Zurücksetzen Ihres Passworts.", | |||
"version": "Version", | |||
"versionShortcut": "V" | |||
} |
@@ -0,0 +1,47 @@ | |||
{ | |||
"Advanced options": "Advanced options", | |||
"Copied": "copied!", | |||
"Counter": "Counter", | |||
"CounterFieldHelp": "Increment this value to change the generated password without changing your master password.", | |||
"CreatePassword": "Would you like to create one?", | |||
"DBNotRunning": "Your LessPass Database is not running", | |||
"DefaultOptionLocalStorage": "We use local storage to save default options locally. Each time you open the app, these options will be loaded by default.", | |||
"Email": "Email", | |||
"EmailAlreadyExist": "This email is already registered. Want to login or recover your password?", | |||
"EmailInvalid": "Please enter a valid email", | |||
"EmailRequiredError": "We need an email to find your account.", | |||
"Encrypt my master password": "Encrypt my master password", | |||
"ForgotPassword": "Forgot your password?", | |||
"Generate": "Generate", | |||
"Length": "Length", | |||
"LessPass Database Url": "LessPass Database Url", | |||
"Login": "Login", | |||
"LoginFormInvalid": "LessPass URL, email, and password are mandatory", | |||
"LoginIncorrectError": "The email and password you entered did not match our records. Please double-check and try again.", | |||
"Master Password": "Master Password", | |||
"Next": "Next", | |||
"NoMatchFor": "Oops! There are no matches for", | |||
"NoPassword": "You don't have any password profile saved in your database.", | |||
"Password profile deleted": "Password profile deleted", | |||
"PasswordProfileCopied": "Your password profile has been copied", | |||
"PasswordResetRequired": "A password is required", | |||
"PasswordResetSuccessful": "Your password was reset successfully.", | |||
"Previous": "Previous", | |||
"Register": "Register", | |||
"Reset my password": "Reset my password", | |||
"ResetLinkExpired": "This password reset link has expired.", | |||
"Save options": "Save options", | |||
"Sign In": "Sign In", | |||
"Site": "Site", | |||
"SiteLoginMasterPasswordMandatory": "Site, login, and master password fields are mandatory.", | |||
"SorryCopy": "We are sorry the copy only works on modern browsers", | |||
"UNDO": "UNDO", | |||
"UpdateYourSearch": "Please try broadening your search.", | |||
"Version": "Version", | |||
"WarningV1Deprecated": "Version 1 is deprecated and will be deleted soon. We strongly advise you to migrate your passwords to version 2.", | |||
"WelcomeRegister": "Welcome {email}, thank you for signing up.", | |||
"Your options have been saved successfully": "Your options have been saved successfully", | |||
"resetPasswordSuccess": "If the email address {email} is associated with a LessPass account, you will shortly receive an email from LessPass with instructions on how to reset your password.", | |||
"version": "version", | |||
"versionShortcut": "v" | |||
} |
@@ -0,0 +1,47 @@ | |||
{ | |||
"Advanced options": "Opciones avanzadas", | |||
"Copied": "¡ copiado !", | |||
"Counter": "Contador", | |||
"CounterFieldHelp": "Aumente este valor para cambiar la contraseña generada sin cambiar su contraseña maestra.", | |||
"CreatePassword": "¿Quiere crear una?", | |||
"DBNotRunning": "Su base de datos de LessPass no está ejecutando", | |||
"DefaultOptionLocalStorage": "Usamos almacenamiento local para guardar las opciones predeterminadas de forma local. Cada vez que abre la aplicación, estas opciones se cargan de forma predeterminada", | |||
"Email": "Correo electrónico", | |||
"EmailAlreadyExist": "Este correo electrónico ya está registrado. ¿Quiere iniciar sesión o recuperar su contraseña?", | |||
"EmailInvalid": "Ingrese un correo elecrónico válido", | |||
"EmailRequiredError": "Necesitamos un correo electrónico para encontrar su cuenta.", | |||
"Encrypt my master password": "Cifrar mi contraseña maestra", | |||
"ForgotPassword": "¿Olvidó su contraseña?", | |||
"Generate": "Generar", | |||
"Length": "Tamaño", | |||
"LessPass Database Url": "URL de la base de datos LessPass", | |||
"Login": "Iniciar sesión", | |||
"LoginFormInvalid": "URL, correo electrónico y contraseña de LessPass URL son obligatorios", | |||
"LoginIncorrectError": "El correo electrónico y la contraseña que ingresó no concuerdan con nuestros registros. Revíselos de nuevo.", | |||
"Master Password": "Contraseña maestra", | |||
"Next": "Después", | |||
"NoMatchFor": "¡Vaya! No ha resultados para", | |||
"NoPassword": "No tiene ningún perfil de contraseñas guardado en su base de datos.", | |||
"Password profile deleted": "Contraseña del perfil de borrado", | |||
"PasswordProfileCopied": "Se ha copiado su perfil de contraseña", | |||
"PasswordResetRequired": "Se requiere una contraseña", | |||
"PasswordResetSuccessful": "Su contraseña ha sido reestablecida con éxito.", | |||
"Previous": "Anterior", | |||
"Register": "Registrar", | |||
"Reset my password": "Reestablecer mi contraseña", | |||
"ResetLinkExpired": "El enlace para reestablecer esta contraseña ha expirado.", | |||
"Save options": "Guardar opciones", | |||
"Sign In": "Registrarse", | |||
"Site": "Sitio", | |||
"SiteLoginMasterPasswordMandatory": "Los campos sitio, usuario y contraseña maestra son obligatorios.", | |||
"SorryCopy": "Lamentamos que la copia sólo funcione en navegadores modernos", | |||
"UNDO": "DESHACER", | |||
"UpdateYourSearch": "Trate de ampliar su búsqueda.", | |||
"Version": "Versión", | |||
"WarningV1Deprecated": "La versión 1 está obsoleta y será eliminada pronto. Le recomendamos enérgicamente migrar sus contraseñas a la versión 2.", | |||
"WelcomeRegister": "Bienvenido o bienvenida {email}, gracias por registrarse.", | |||
"Your options have been saved successfully": "Sus opciones se han guardado correctamente", | |||
"resetPasswordSuccess": "Si la dirección de correo electrónico {email} está asociada a una cuenta LessPass, recibirá un correo electrónico de LessPass con instrucciones sobre cómo restablecer su contraseña.", | |||
"version": "versión", | |||
"versionShortcut": "v" | |||
} |
@@ -0,0 +1,47 @@ | |||
{ | |||
"Advanced options": "Options avancées", | |||
"Copied": "Copié !", | |||
"Counter": "Compteur", | |||
"CounterFieldHelp": "Augmenter cette valeur pour changer de mot de passe sans changer de mot de passe fort.", | |||
"CreatePassword": "Voulez-vous en créer un ?", | |||
"DBNotRunning": "Votre base de données LessPass n'est pas démarrée.", | |||
"DefaultOptionLocalStorage": "Pour sauvegarder les options par default, nous utilisons le stockage du navigateur. Chaque fois que vous ouvrez l'application, ces options sont chargées par defaut.", | |||
"Email": "Email", | |||
"EmailAlreadyExist": "Cet email est déjà enregistré. Vous voulez peut-être vous connecter ?", | |||
"EmailInvalid": "Entrez un email valide", | |||
"EmailRequiredError": "Nous avons besoin d'un email pour trouver votre compte.", | |||
"Encrypt my master password": "Chiffrer mon mot de passe fort", | |||
"ForgotPassword": "Mot de passe oublié ?", | |||
"Generate": "Générer", | |||
"Length": "Longueur", | |||
"LessPass Database Url": "Url de LessPass Database", | |||
"Login": "Login", | |||
"LoginFormInvalid": "L'URL LessPass, l'email et le mot de passe sont obligatoires.", | |||
"LoginIncorrectError": "L'email et le mot de passe ne sont pas dans notre base de données. Vérifiez une nouvelle fois et réessayez.", | |||
"Master Password": "Mot de passe fort", | |||
"Next": "Suivant", | |||
"NoMatchFor": "Oups ! il n'y a aucun resultat pour", | |||
"NoPassword": "Vous n'avez aucun mot de passe enregistré.", | |||
"Password profile deleted": "Profil de mot de passe supprimé", | |||
"PasswordProfileCopied": "Votre profil de mot de passe a été copié", | |||
"PasswordResetRequired": "Un mot de passe est requis", | |||
"PasswordResetSuccessful": "Votre mot de passe a été changé avec succès", | |||
"Previous": "Précédent", | |||
"Register": "S'enregistrer", | |||
"Reset my password": "Changer mon mot de passe", | |||
"ResetLinkExpired": "Ce lien a expiré.", | |||
"Save options": "Enregistrer les options", | |||
"Sign In": "Se connecter", | |||
"Site": "Site", | |||
"SiteLoginMasterPasswordMandatory": "Les champs site, login et mot de passe fort sont obligatoires.", | |||
"SorryCopy": "Nous sommes désolés, la copie ne fonctionne que sur les navigateurs modernes", | |||
"UNDO": "ANNULER", | |||
"UpdateYourSearch": "Merci de modifier votre recherche.", | |||
"Version": "Version", | |||
"WarningV1Deprecated": "La version 1 est déconseillée et sera supprimée bientôt. Nous vous conseillons fortement de migrer vos mots de passe vers la version 2.", | |||
"WelcomeRegister": "Bienvenue {email}, merci pour vous être enregistré.", | |||
"Your options have been saved successfully": "Vos options ont été enregistrées avec succès", | |||
"resetPasswordSuccess": "Si l'adresse email {email} est associée avec un compte LessPass, vous allez recevoir un email de la part de LessPass avec les instructions pour changer votre mot de passe.", | |||
"version": "version", | |||
"versionShortcut": "v" | |||
} |
@@ -0,0 +1,47 @@ | |||
{ | |||
"Advanced options": "高级选项", | |||
"Copied": "已复制", | |||
"Counter": "计数器", | |||
"CounterFieldHelp": "增加这个值就可以在不改变主密码的前提下生成全新的密码。", | |||
"CreatePassword": "您要生成一个密码吗?", | |||
"DBNotRunning": "您的 LessPass 数据库没有运行", | |||
"DefaultOptionLocalStorage": "我们使用“本地存储”在本机保存默认选项。每次您开启本应用时,会默认加载这些选项。", | |||
"Email": "邮件地址", | |||
"EmailAlreadyExist": "这个邮件地址已被注册。登录或找回您的密码?", | |||
"EmailInvalid": "请输入一个有效的电子邮件地址", | |||
"EmailRequiredError": "我们需要一个电子邮件地址来找到您的账户。", | |||
"Encrypt my master password": "加密我的主密码", | |||
"ForgotPassword": "忘记了您的密码?", | |||
"Generate": "生成", | |||
"Length": "长度", | |||
"LessPass Database Url": "LessPass 数据库网址", | |||
"Login": "登录名", | |||
"LoginFormInvalid": "LessPass 网址、电子邮件地址以及密码均为必填信息。", | |||
"LoginIncorrectError": "我们没找到符合您输入的电子邮件地址及密码的记录。请核验后再试。", | |||
"Master Password": "主密码", | |||
"Next": "下一步", | |||
"NoMatchFor": "没有找到符合下列条件的内容:", | |||
"NoPassword": "您的数据库里没有保存任何密码配置。", | |||
"Password profile deleted": "密码配置文件被删", | |||
"PasswordProfileCopied": "已复制您的密码配置。", | |||
"PasswordResetRequired": "请输入登录密码", | |||
"PasswordResetSuccessful": "已成功重置您的登录密码。", | |||
"Previous": "上一步", | |||
"Register": "注册", | |||
"Reset my password": "重置我的登录密码", | |||
"ResetLinkExpired": "此登录密码重置链接已过期。", | |||
"Save options": "保存选项", | |||
"Sign In": "登录", | |||
"Site": "网站名", | |||
"SiteLoginMasterPasswordMandatory": "网站名、登录名以及主密码均为必填信息。", | |||
"SorryCopy": "很抱歉,但复制功能仅适用于现代浏览器", | |||
"UNDO": "解开", | |||
"UpdateYourSearch": "请尝试放宽您的搜索条件。", | |||
"Version": "版本", | |||
"WarningV1Deprecated": "版本 1 已不再支持,不久后将被删除。我们强烈建议您将密码迁移至版本 2。", | |||
"WelcomeRegister": "你好 {email},欢迎您的注册。", | |||
"Your options have been saved successfully": "您的选项已成功保存", | |||
"resetPasswordSuccess": "如果电子邮件地址 {email} 与一个 LessPass 账户相关联,您将很快收到 LessPass 的电子邮件,里面提供有重置密码的操作说明。", | |||
"version": "版本", | |||
"versionShortcut": "v" | |||
} |
@@ -0,0 +1,47 @@ | |||
{ | |||
"Advanced options": "進階選項", | |||
"Copied": "已複製", | |||
"Counter": "計數器", | |||
"CounterFieldHelp": "改變這個值,就可以在不改變主密碼的狀況下,產生新的密碼。", | |||
"CreatePassword": "您要產生一組密碼嗎?", | |||
"DBNotRunning": "您的 LessPass 資料庫並未執行", | |||
"DefaultOptionLocalStorage": "我們將預設選項儲存在本機。每當您開啟程式,這些設定將會被自動載入。", | |||
"Email": "郵件位址", | |||
"EmailAlreadyExist": "這個郵件位址已被註冊。想要登入或取回您的密碼嗎?", | |||
"EmailInvalid": "請輸入一個有效的郵件位址", | |||
"EmailRequiredError": "我們需要郵件位址來找到您的帳號。", | |||
"Encrypt my master password": "加密我的主密码", | |||
"ForgotPassword": "忘記您的登入密碼了嗎?", | |||
"Generate": "產生", | |||
"Length": "長度", | |||
"LessPass Database Url": "LessPass數據庫URL", | |||
"Login": "登入帳號", | |||
"LoginFormInvalid": "LessPass URL、郵件位址、登入密碼皆為必填欄位。", | |||
"LoginIncorrectError": "我們查不到您輸入的郵件位址及登入密碼。請確認後再試一次。", | |||
"Master Password": "主密碼", | |||
"Next": "然後", | |||
"NoMatchFor": "喔不!沒有找到跟下列條件相似的結果:", | |||
"NoPassword": "您的資料庫內沒有儲存任何密碼。", | |||
"Password profile deleted": "密碼配置文件被刪", | |||
"PasswordProfileCopied": "您的密码配置文件已被复制。", | |||
"PasswordResetRequired": "請輸入登入密碼", | |||
"PasswordResetSuccessful": "已成功重置您的登入密碼。", | |||
"Previous": "以前", | |||
"Register": "註冊", | |||
"Reset my password": "重置我的登入密碼", | |||
"ResetLinkExpired": "此登入密碼重設連結已過期。", | |||
"Save options": "保存選項", | |||
"Sign In": "登入", | |||
"Site": "網站位址", | |||
"SiteLoginMasterPasswordMandatory": "網站位址、登入帳號、主密碼皆為必填欄位。", | |||
"SorryCopy": "我們很抱歉,該副本僅適用於現代瀏覽器", | |||
"UNDO": "解開", | |||
"UpdateYourSearch": "請試著放寬您的搜尋條件。", | |||
"Version": "版本", | |||
"WarningV1Deprecated": "版本 1 已不支援,不久將被刪除。 我們強烈得建議您將密碼換至版本 2。", | |||
"WelcomeRegister": "歡迎 {email},謝謝您的註冊。", | |||
"Your options have been saved successfully": "您的選項已成功保存", | |||
"resetPasswordSuccess": "如果电子邮件地址 {email} 与LessPass帐户相关联,您将很快收到LessPass的电子邮件,并提供如何重置密码的说明。", | |||
"version": "版本", | |||
"versionShortcut": "v" | |||
} |
@@ -0,0 +1,20 @@ | |||
<!DOCTYPE html> | |||
<html> | |||
<head> | |||
<meta http-equiv="Content-Security-Policy" | |||
content="default-src 'self' data: gap: https://ssl.gstatic.com 'unsafe-eval'; style-src 'self' 'unsafe-inline'; media-src *; img-src 'self' data: content:; connect-src *;"> | |||
<meta name="format-detection" content="telephone=no"> | |||
<meta name="msapplication-tap-highlight" content="no"> | |||
<meta name="viewport" | |||
content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width"> | |||
<link rel="stylesheet" href="dist/lesspass.min.css"> | |||
<title>LessPass</title> | |||
</head> | |||
<body> | |||
<div class="lesspass--unbordered lesspass--full-width"> | |||
<div id="lesspass"></div> | |||
</div> | |||
<script type="text/javascript" src="cordova.js"></script> | |||
<script src="dist/lesspass.min.js"></script> | |||
</body> | |||
</html> |
@@ -0,0 +1,46 @@ | |||
/* | |||
* Licensed to the Apache Software Foundation (ASF) under one | |||
* or more contributor license agreements. See the NOTICE file | |||
* distributed with this work for additional information | |||
* regarding copyright ownership. The ASF licenses this file | |||
* to you under the Apache License, Version 2.0 (the | |||
* "License"); you may not use this file except in compliance | |||
* with the License. You may obtain a copy of the License at | |||
* | |||
* http://www.apache.org/licenses/LICENSE-2.0 | |||
* | |||
* Unless required by applicable law or agreed to in writing, | |||
* software distributed under the License is distributed on an | |||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |||
* KIND, either express or implied. See the License for the | |||
* specific language governing permissions and limitations | |||
* under the License. | |||
*/ | |||
var app = { | |||
// Application Constructor | |||
initialize: function() { | |||
document.addEventListener('deviceready', this.onDeviceReady.bind(this), false); | |||
}, | |||
// deviceready Event Handler | |||
// | |||
// Bind any cordova events here. Common events are: | |||
// 'pause', 'resume', etc. | |||
onDeviceReady: function() { | |||
this.receivedEvent('deviceready'); | |||
}, | |||
// Update DOM on a Received Event | |||
receivedEvent: function(id) { | |||
var parentElement = document.getElementById(id); | |||
var listeningElement = parentElement.querySelector('.listening'); | |||
var receivedElement = parentElement.querySelector('.received'); | |||
listeningElement.setAttribute('style', 'display:none;'); | |||
receivedElement.setAttribute('style', 'display:block;'); | |||
console.log('Received Event: ' + id); | |||
} | |||
}; | |||
app.initialize(); |
@@ -1 +0,0 @@ | |||
Subproject commit b6ed2c7b03963404ea3e0ad622e19bf6bc468b2b |
@@ -0,0 +1,17 @@ | |||
# editorconfig.org | |||
root = true | |||
[*] | |||
charset = utf-8 | |||
end_of_line = lf | |||
indent_size = 2 | |||
indent_style = space | |||
insert_final_newline = true | |||
trim_trailing_whitespace = true | |||
[*.md] | |||
trim_trailing_whitespace = false | |||
[*.py] | |||
indent_size = 4 |
@@ -0,0 +1,2 @@ | |||
node_modules/ | |||
npm-debug.log |
@@ -0,0 +1,17 @@ | |||
dist: trusty | |||
sudo: required | |||
language: node_js | |||
node_js: | |||
- 6 | |||
addons: | |||
firefox: "latest" | |||
apt: | |||
sources: | |||
- google-chrome | |||
packages: | |||
- google-chrome-stable | |||
before_script: | |||
- export CHROME_BIN=/usr/bin/google-chrome | |||
- "export DISPLAY=:99.0" | |||
- "sh -e /etc/init.d/xvfb start" | |||
- sleep 3 |
@@ -0,0 +1,89 @@ | |||
# LessPass core | |||
npm core library used to generate LessPass passwords | |||
## Requirements | |||
- node LTS v6 | |||
## Install | |||
npm install lesspass | |||
## Usage | |||
var profile = { | |||
site: 'example.org', | |||
login: 'contact@example.org' | |||
} | |||
var masterPassword = 'password'; | |||
LessPass.generatePassword(profile, masterPassword) | |||
.then(function (generatedPassword) { | |||
console.log(generatedPassword); // WHLpUL)e00[iHR+w | |||
}); | |||
see [tests/api.tests.js](tests/v2/api.tests.js) for more examples | |||
## API | |||
### generatePassword(profile, masterPassword) | |||
generate LessPass password | |||
var profile = { | |||
site: 'example.org', | |||
login: 'contact@example.org' | |||
options: { | |||
uppercase: true, | |||
lowercase: true, | |||
digits: true, | |||
symbols: true, | |||
length: 16, | |||
counter: 1 | |||
}, | |||
crypto: { | |||
method: 'pbkdf2', | |||
iterations: 100000, | |||
keylen: 32, | |||
digest: "sha256" | |||
} | |||
}; | |||
var masterPassword = 'password'; | |||
LessPass.generatePassword(profile, masterPassword) | |||
.then(function (generatedPassword) { | |||
console.log(generatedPassword); // WHLpUL)e00[iHR+w | |||
}); | |||
### createFingerprint(password) | |||
create a fingerprint | |||
LessPass.createFingerprint('password').then(fingerprint => { | |||
console.log(fingerprint); //e56a207acd1e6714735487c199c6f095844b7cc8e5971d86c003a7b6f36ef51e | |||
}); | |||
### isSupported() | |||
test if LessPass is supported | |||
LessPass.isSupported().then(function(isSupported) { | |||
if (isSupported) { | |||
console.log("LessPass is supported"); | |||
} | |||
else { | |||
console.log("LessPass is not supported"); | |||
} | |||
}); | |||
## Tests | |||
npm test | |||
## License | |||
This project is licensed under the terms of the GNU GPLv3. | |||
## Issues | |||
report issues on [LessPass project](https://github.com/lesspass/lesspass/issues) |
@@ -0,0 +1,49 @@ | |||
<!doctype html> | |||
<html lang="en"> | |||
<head> | |||
<meta charset="UTF-8"> | |||
</head> | |||
<body> | |||
<script src="../dist/lesspass.min.js"></script> | |||
<script> | |||
function displayInBody(txt) { | |||
document.body.innerHTML = document.body.innerHTML + txt + '<br>'; | |||
} | |||
function displayErrorInBody(txt) { | |||
document.body.innerHTML = document.body.innerHTML + '<span style="color:red;">' + txt + '</span><br>'; | |||
} | |||
function createFingerprint() { | |||
LessPass.createFingerprint('password').then(fingerprint => { | |||
displayInBody("createFingerprint :"); | |||
displayInBody("e56a207acd1e6714735487c199c6f095844b7cc8e5971d86c003a7b6f36ef51e"); | |||
displayInBody(fingerprint + " (generated)") | |||
}); | |||
} | |||
function generatePassword() { | |||
const profile = { | |||
site: 'example.org', | |||
login: 'contact@example.org' | |||
} | |||
const masterPassword = 'password'; | |||
LessPass.generatePassword(profile, masterPassword).then(function(generatedPassword) { | |||
displayInBody("<br>generatePassword :"); | |||
displayInBody("WHLpUL)e00[iHR+w"); | |||
displayInBody(generatedPassword + " (generated)") | |||
}); | |||
} | |||
LessPass.isSupported().then(function(isSupported) { | |||
if (isSupported) { | |||
createFingerprint(); | |||
generatePassword(); | |||
} | |||
else { | |||
displayErrorInBody("LessPass is not supported on your browser!"); | |||
} | |||
}); | |||
</script> | |||
</body> | |||
</html> |
@@ -0,0 +1,57 @@ | |||
{ | |||
"name": "lesspass", | |||
"version": "8.0.1", | |||
"description": "LessPass node module used to generate LessPass passwords", | |||
"keywords": [ | |||
"crypto", | |||
"lesspass", | |||
"password" | |||
], | |||
"license": "GPL-3.0", | |||
"author": "Guillaume Vincent <guillaume@oslab.fr>", | |||
"files": [ | |||
"dist", | |||
"src" | |||
], | |||
"main": "src/lesspass.js", | |||
"browser": { | |||
"./src/pbkdf2.js": "./src/pbkdf2.browser.js", | |||
"./src/hmac.js": "./src/hmac.browser.js" | |||
}, | |||
"module": "src/lesspass.js", | |||
"jsnext:main": "src/lesspass.js", | |||
"repository": "lesspass/core", | |||
"scripts": { | |||
"precommit": "npm run test:unit && lint-staged", | |||
"clean": "rm -rf dist && mkdir dist && npm prune", | |||
"build": "npm run clean && browserify --standalone LessPass src/lesspass.js > dist/lesspass.js && npm run minify", | |||
"minify": "uglifyjs --output dist/lesspass.min.js --compress --mangle -- dist/lesspass.js", | |||
"test": "npm run test:unit && npm run test:browser", | |||
"test:unit": "mocha test --recursive", | |||
"test:browser": "npm run build && karma start test/karma.conf.js" | |||
}, | |||
"dependencies": { | |||
"lesspass-render-password": "^0.1.0", | |||
"lodash.merge": "^4.6.0", | |||
"unibabel": "2.1.4" | |||
}, | |||
"devDependencies": { | |||
"browserify": "^14.3.0", | |||
"husky": "^0.14.3", | |||
"karma": "^1.6.0", | |||
"karma-browserify": "^5.1.1", | |||
"karma-chrome-launcher": "^2.0.0", | |||
"karma-firefox-launcher": "^1.0.1", | |||
"karma-mocha": "^1.3.0", | |||
"lint-staged": "^4.3.0", | |||
"mocha": "^4.0.1", | |||
"prettier": "^1.2.2", | |||
"uglify-js": "^3.0.1" | |||
}, | |||
"lint-staged": { | |||
"{src,test}/**/*.js": [ | |||
"prettier --write", | |||
"git add" | |||
] | |||
} | |||
} |
@@ -0,0 +1,31 @@ | |||
require("unibabel"); | |||
require("unibabel/unibabel.hex"); | |||
module.exports = function(digest, string, salt) { | |||
var algorithms = { | |||
sha1: "SHA-1", | |||
"sha-1": "SHA-1", | |||
sha256: "SHA-256", | |||
"sha-256": "SHA-256", | |||
sha512: "SHA-512", | |||
"sha-512": "SHA-512" | |||
}; | |||
return window.crypto.subtle | |||
.importKey( | |||
"raw", | |||
Unibabel.utf8ToBuffer(string), | |||
{ | |||
name: "HMAC", | |||
hash: { name: algorithms[digest.toLowerCase()] } | |||
}, | |||
true, | |||
["sign", "verify"] | |||
) | |||
.then(function(key) { | |||
return window.crypto.subtle | |||
.sign({ name: "HMAC" }, key, Unibabel.utf8ToBuffer(salt || "")) | |||
.then(function(signature) { | |||
return Unibabel.bufferToHex(new Uint8Array(signature)); | |||
}); | |||
}); | |||
}; |
@@ -0,0 +1,8 @@ | |||
var crypto = require("crypto"); | |||
module.exports = function(digest, string, salt) { | |||
return new Promise(function(resolve) { | |||
var hmac = crypto.createHmac(digest, string); | |||
resolve(hmac.update(salt || "").digest("hex")); | |||
}); | |||
}; |
@@ -0,0 +1,69 @@ | |||
var hmac = require("./hmac"); | |||
var pbkdf2 = require("./pbkdf2"); | |||
var merge = require("lodash.merge"); | |||
var renderPassword = require("lesspass-render-password"); | |||
var defaultProfile = { | |||
site: "", | |||
login: "", | |||
options: { | |||
uppercase: true, | |||
lowercase: true, | |||
digits: true, | |||
symbols: true, | |||
length: 16, | |||
counter: 1 | |||
}, | |||
crypto: { | |||
method: "pbkdf2", | |||
iterations: 100000, | |||
keylen: 32, | |||
digest: "sha256" | |||
} | |||
}; | |||
module.exports = { | |||
generatePassword: generatePassword, | |||
createFingerprint: createFingerprint, | |||
isSupported: isSupported, | |||
_calcEntropy: _calcEntropy | |||
}; | |||
function generatePassword(profile, masterPassword) { | |||
var _profile = merge({}, defaultProfile, profile); | |||
return _calcEntropy(_profile, masterPassword).then(function(entropy) { | |||
return renderPassword(entropy, _profile.options); | |||
}); | |||
} | |||
function createFingerprint(str) { | |||
return hmac("sha256", str); | |||
} | |||
function isSupported() { | |||
try { | |||
var simpleProfile = merge({}, defaultProfile, { | |||
crypto: { iterations: 1 } | |||
}); | |||
return generatePassword(simpleProfile, "LessPass").then(function( | |||
generatedPassword | |||
) { | |||
return generatedPassword === "n'LTsjPA#3E$e*2'"; | |||
}); | |||
} catch (e) { | |||
console.error(e); | |||
return Promise.resolve(false); | |||
} | |||
} | |||
function _calcEntropy(profile, masterPassword) { | |||
var salt = | |||
profile.site + profile.login + profile.options.counter.toString(16); | |||
return pbkdf2( | |||
masterPassword, | |||
salt, | |||
profile.crypto.iterations, | |||
profile.crypto.keylen, | |||
profile.crypto.digest | |||
); | |||
} |
@@ -0,0 +1,42 @@ | |||
require("unibabel"); | |||
require("unibabel/unibabel.hex"); | |||
module.exports = function(password, salt, iterations, keylen, digest) { | |||
var algorithms = { | |||
sha1: "SHA-1", | |||
"sha-1": "SHA-1", | |||
sha256: "SHA-256", | |||
"sha-256": "SHA-256", | |||
sha512: "SHA-512", | |||
"sha-512": "SHA-512" | |||
}; | |||
return window.crypto.subtle | |||
.importKey("raw", Unibabel.utf8ToBuffer(password), "PBKDF2", false, [ | |||
"deriveKey" | |||
]) | |||
.then(function(key) { | |||
var algo = { | |||
name: "PBKDF2", | |||
salt: Unibabel.utf8ToBuffer(salt), | |||
iterations: iterations, | |||
hash: algorithms[digest.toLowerCase()] | |||
}; | |||
return window.crypto.subtle.deriveKey( | |||
algo, | |||
key, | |||
{ | |||
name: "AES-CTR", | |||
length: keylen * 8 | |||
}, | |||
true, | |||
["encrypt", "decrypt"] | |||
); | |||
}) | |||
.then(function(derivedKey) { | |||
return window.crypto.subtle | |||
.exportKey("raw", derivedKey) | |||
.then(function(keyArray) { | |||
return Unibabel.bufferToHex(new Uint8Array(keyArray)); | |||
}); | |||
}); | |||
}; |
@@ -0,0 +1,16 @@ | |||
const crypto = require("crypto"); | |||
module.exports = function(password, salt, iterations, keylen, digest) { | |||
return new Promise(function(resolve, reject) { | |||
crypto.pbkdf2(password, salt, iterations, keylen, digest, function( | |||
error, | |||
key | |||
) { | |||
if (error) { | |||
reject("error in pbkdf2"); | |||
} else { | |||
resolve(key.toString("hex")); | |||
} | |||
}); | |||
}); | |||
}; |
@@ -0,0 +1,98 @@ | |||
var assert = require("assert"); | |||
var LessPass = require("../src/lesspass"); | |||
describe("api", () => { | |||
it("generatePassword", () => { | |||
const profile = { | |||
site: "example.org", | |||
login: "contact@example.org", | |||
options: { | |||
lowercase: true, | |||
uppercase: true, | |||
digits: true, | |||
symbols: true, | |||
length: 16, | |||
counter: 1 | |||
} | |||
}; | |||
const masterPassword = "password"; | |||
return LessPass | |||
.generatePassword(profile, masterPassword) | |||
.then(generatedPassword => { | |||
assert.equal("WHLpUL)e00[iHR+w", generatedPassword); | |||
}); | |||
}); | |||
it("generatePassword default options", () => { | |||
const profile = { | |||
site: "example.org", | |||
login: "contact@example.org" | |||
}; | |||
const masterPassword = "password"; | |||
return LessPass.generatePassword(profile, masterPassword).then(generatedPassword => { | |||
assert.equal("WHLpUL)e00[iHR+w", generatedPassword); | |||
}); | |||
}); | |||
it("generatedPassword different options", () => { | |||
const profile = { | |||
site: "example.org", | |||
login: "contact@example.org", | |||
options: { | |||
lowercase: true, | |||
uppercase: true, | |||
digits: true, | |||
symbols: false, | |||
length: 14, | |||
counter: 2 | |||
} | |||
}; | |||
const masterPassword = "password"; | |||
return LessPass.generatePassword(profile, masterPassword).then(generatedPassword => { | |||
assert.equal("MBAsB7b1Prt8Sl", generatedPassword); | |||
assert.equal(14, generatedPassword.length); | |||
}); | |||
}); | |||
it("generatedPassword only digits", () => { | |||
const profile = { | |||
site: "example.org", | |||
login: "contact@example.org", | |||
options: { | |||
lowercase: false, | |||
uppercase: false, | |||
digits: true, | |||
symbols: false, | |||
length: 6, | |||
counter: 3 | |||
} | |||
}; | |||
const masterPassword = "password"; | |||
return LessPass.generatePassword(profile, masterPassword).then(generatedPassword => { | |||
assert.equal("117843", generatedPassword); | |||
}); | |||
}); | |||
it("generatedPassword no digit", () => { | |||
const profile = { | |||
site: "example.org", | |||
login: "contact@example.org", | |||
options: { | |||
digits: false | |||
} | |||
}; | |||
const masterPassword = "password"; | |||
return LessPass.generatePassword(profile, masterPassword).then(generatedPassword => { | |||
assert.equal("s>{F}RwkN/-fmM.X", generatedPassword); | |||
}); | |||
}); | |||
it("createFingerprint", () => { | |||
return LessPass.createFingerprint("password").then(function(fingerprint) { | |||
assert.equal( | |||
"e56a207acd1e6714735487c199c6f095844b7cc8e5971d86c003a7b6f36ef51e", | |||
fingerprint | |||
); | |||
}); | |||
}); | |||
it("isSupported", () => { | |||
return LessPass.isSupported("password").then(function(isSupported) { | |||
assert(isSupported); | |||
}); | |||
}); | |||
}); |
@@ -0,0 +1,109 @@ | |||
var assert = require("assert"); | |||
var LessPass = require("../src/lesspass"); | |||
describe("entropy", function() { | |||
it("calc entropy pbkdf2 with default params (100000 iterations, 32 bytes length, sha256 digest)", function() { | |||
const profile = { | |||
site: "example.org", | |||
login: "contact@example.org", | |||
options: { | |||
counter: 1 | |||
}, | |||
crypto: { | |||
method: "pbkdf2", | |||
iterations: 100000, | |||
keylen: 32, | |||
digest: "sha256" | |||
} | |||
}; | |||
const masterPassword = "password"; | |||
return LessPass._calcEntropy(profile, masterPassword).then(function( | |||
entropy | |||
) { | |||
assert.equal( | |||
"dc33d431bce2b01182c613382483ccdb0e2f66482cbba5e9d07dab34acc7eb1e", | |||
entropy | |||
); | |||
}); | |||
}); | |||
it("calc entropy pbkdf2 with unicode char", function() { | |||
const profile = { | |||
site: "example.org", | |||
login: "❤", | |||
options: { | |||
counter: 1 | |||
}, | |||
crypto: { | |||
method: "pbkdf2", | |||
iterations: 100000, | |||
keylen: 32, | |||
digest: "sha256" | |||
} | |||
}; | |||
const masterPassword = "I ❤ LessPass"; | |||
return LessPass._calcEntropy(profile, masterPassword).then(function( | |||
entropy | |||
) { | |||
assert.equal( | |||
"4e66cab40690c01af55efd595f5963cc953d7e10273c01827881ebf8990c627f", | |||
entropy | |||
); | |||
}); | |||
}); | |||
it("calc entropy with different options (8192 iterations, 16 bytes length, sha512 digest)", function() { | |||
const profile = { | |||
site: "example.org", | |||
login: "contact@example.org", | |||
options: { | |||
counter: 1 | |||
}, | |||
crypto: { | |||
method: "pbkdf2", | |||
iterations: 8192, | |||
keylen: 16, | |||
digest: "sha512" | |||
} | |||
}; | |||
const masterPassword = "password"; | |||
return LessPass._calcEntropy(profile, masterPassword).then(function( | |||
entropy | |||
) { | |||
assert.equal("fff211c16a4e776b3574c6a5c91fd252", entropy); | |||
}); | |||
}); | |||
it("calc entropy different if counter different 1", function() { | |||
const profile = { | |||
site: "example.org", | |||
login: "contact@example.org", | |||
options: { | |||
counter: 1 | |||
}, | |||
crypto: { | |||
method: "pbkdf2", | |||
iterations: 100000, | |||
keylen: 32, | |||
digest: "sha256" | |||
} | |||
}; | |||
const profile2 = { | |||
site: "example.org", | |||
login: "contact@example.org", | |||
options: { | |||
counter: 2 | |||
}, | |||
crypto: { | |||
method: "pbkdf2", | |||
iterations: 100000, | |||
keylen: 32, | |||
digest: "sha256" | |||
} | |||
}; | |||
const promises = [ | |||
LessPass._calcEntropy(profile, "password"), | |||
LessPass._calcEntropy(profile2, "password") | |||
]; | |||
Promise.all(promises).then(values => { | |||
assert.notEqual(values[0], values[1]); | |||
}); | |||
}); | |||
}); |
@@ -0,0 +1,21 @@ | |||
var assert = require("assert"); | |||
var createHmac = require("../src/hmac"); | |||
describe("hmac", function() { | |||
it("createHmac", function() { | |||
return createHmac("sha256", "password").then(function(fingerprint) { | |||
assert.equal( | |||
"e56a207acd1e6714735487c199c6f095844b7cc8e5971d86c003a7b6f36ef51e", | |||
fingerprint | |||
); | |||
}); | |||
}); | |||
it("createHmac and update", function() { | |||
return createHmac("sha256", "password", "salt").then(function(fingerprint) { | |||
assert.equal( | |||
"fc328232993ff34ca56631e4a101d60393cad12171997ee0b562bf7852b2fed0", | |||
fingerprint | |||
); | |||
}); | |||
}); | |||
}); |
@@ -0,0 +1,19 @@ | |||
module.exports = function(config) { | |||
config.set({ | |||
basePath: "..", | |||
frameworks: ["browserify", "mocha"], | |||
files: ["dist/lesspass.min.js", "test/**/*.js"], | |||
exclude: [], | |||
preprocessors: { | |||
"test/**/*.js": ["browserify"] | |||
}, | |||
reporters: ["progress"], | |||
port: 9876, | |||
colors: true, | |||
logLevel: config.LOG_INFO, | |||
autoWatch: false, | |||
browsers: ["Firefox", "Chrome"], | |||
singleRun: true, | |||
concurrency: Infinity | |||
}); | |||
}; |
@@ -0,0 +1,227 @@ | |||
var assert = require("assert"); | |||
var pbkdf2 = require("../src/pbkdf2"); | |||
describe("pbkdf2", function() { | |||
it("secret, salt, 2 iterations, 32 keylen, sha256 hash", function() { | |||
return pbkdf2("secret", "salt", 2, 32, "sha256").then(function(key) { | |||
assert.equal( | |||
"f92f45f9df4c2aeabae1ed3c16f7b64660c1f8e377fa9b4699b23c2c3a29f569", | |||
key | |||
); | |||
}); | |||
}); | |||
it("use pbkdf2 with 8192 iterations and sha256", function() { | |||
return pbkdf2( | |||
"password", | |||
"test@example.org", | |||
8192, | |||
32, | |||
"sha256" | |||
).then(function(key) { | |||
assert.equal( | |||
"d8af5f918db6b65b1db3d3984e5a400e39e1dbb19462220e4431de283809f472", | |||
key | |||
); | |||
}); | |||
}); | |||
it("customize number of iterations", function() { | |||
return pbkdf2( | |||
"password", | |||
"test@example.org", | |||
4096, | |||
32, | |||
"sha256" | |||
).then(function(key) { | |||
assert.equal( | |||
"0a91208545e3aa4935d3a22984ca097a7669259a04d261ac16361bdc1a2e960f", | |||
key | |||
); | |||
}); | |||
}); | |||
it("customize key length", function() { | |||
return pbkdf2( | |||
"password", | |||
"test@example.org", | |||
8192, | |||
16, | |||
"sha256" | |||
).then(function(key) { | |||
assert.equal("d8af5f918db6b65b1db3d3984e5a400e", key); | |||
}); | |||
}); | |||
it("customize iterations and key length", function() { | |||
return pbkdf2( | |||
"password", | |||
"test@example.org", | |||
4096, | |||
16, | |||
"sha256" | |||
).then(function(key) { | |||
assert.equal("0a91208545e3aa4935d3a22984ca097a", key); | |||
}); | |||
}); | |||
it("utf8 parameter", function() { | |||
return pbkdf2( | |||
"♥ LessPass ♥", | |||
"test@example.org", | |||
8192, | |||
32, | |||
"sha256" | |||
).then(function(key) { | |||
assert.equal( | |||
"997fe81d3d0db236e039c75efdb487f17a902fdf94f9dacaa9884329c85d9651", | |||
key | |||
); | |||
}); | |||
}); | |||
it("auto generated test 0", function() { | |||
return pbkdf2( | |||
"password", | |||
"contact@lesspass.com", | |||
8192, | |||
32, | |||
"sha256" | |||
).then(function(key) { | |||
assert.equal( | |||
"63d850713d0b2f7f2c4396fe93f4ac0c6bc7485f9e7473c4b8c4a33ec12199c0", | |||
key | |||
); | |||
}); | |||
}); | |||
it("auto generated test 1", function() { | |||
return pbkdf2( | |||
"password", | |||
"contact@lesspass.com", | |||
8192, | |||
32, | |||
"sha256" | |||
).then(function(key) { | |||
assert.equal( | |||
"63d850713d0b2f7f2c4396fe93f4ac0c6bc7485f9e7473c4b8c4a33ec12199c0", | |||
key | |||
); | |||
}); | |||
}); | |||
it("auto generated test 2", function() { | |||
return pbkdf2( | |||
"password", | |||
"contact@lesspass.com", | |||
8192, | |||
32, | |||
"sha256" | |||
).then(function(key) { | |||
assert.equal( | |||
"63d850713d0b2f7f2c4396fe93f4ac0c6bc7485f9e7473c4b8c4a33ec12199c0", | |||
key | |||
); | |||
}); | |||
}); | |||
it("auto generated test 3", function() { | |||
return pbkdf2( | |||
"password", | |||
"contact@lesspass.com", | |||
8192, | |||
32, | |||
"sha256" | |||
).then(function(key) { | |||
assert.equal( | |||
"63d850713d0b2f7f2c4396fe93f4ac0c6bc7485f9e7473c4b8c4a33ec12199c0", | |||
key | |||
); | |||
}); | |||
}); | |||
it("auto generated test 4", function() { | |||
return pbkdf2( | |||
"password", | |||
"contact@lesspass.com", | |||
8192, | |||
32, | |||
"sha256" | |||
).then(function(key) { | |||
assert.equal( | |||
"63d850713d0b2f7f2c4396fe93f4ac0c6bc7485f9e7473c4b8c4a33ec12199c0", | |||
key | |||
); | |||
}); | |||
}); | |||
it("auto generated test 5", function() { | |||
return pbkdf2( | |||
"password", | |||
"contact@lesspass.com", | |||
8192, | |||
32, | |||
"sha256" | |||
).then(function(key) { | |||
assert.equal( | |||
"63d850713d0b2f7f2c4396fe93f4ac0c6bc7485f9e7473c4b8c4a33ec12199c0", | |||
key | |||
); | |||
}); | |||
}); | |||
it("auto generated test 6", function() { | |||
return pbkdf2( | |||
"password", | |||
"contact@lesspass.com", | |||
8192, | |||
32, | |||
"sha256" | |||
).then(function(key) { | |||
assert.equal( | |||
"63d850713d0b2f7f2c4396fe93f4ac0c6bc7485f9e7473c4b8c4a33ec12199c0", | |||
key | |||
); | |||
}); | |||
}); | |||
it("auto generated test 7", function() { | |||
return pbkdf2( | |||
"password", | |||
"contact@lesspass.com", | |||
8192, | |||
32, | |||
"sha256" | |||
).then(function(key) { | |||
assert.equal( | |||
"63d850713d0b2f7f2c4396fe93f4ac0c6bc7485f9e7473c4b8c4a33ec12199c0", | |||
key | |||
); | |||
}); | |||
}); | |||
it("auto generated test 8", function() { | |||
return pbkdf2( | |||
"password", | |||
"contact@lesspass.com", | |||
8192, | |||
32, | |||
"sha256" | |||
).then(function(key) { | |||
assert.equal( | |||
"63d850713d0b2f7f2c4396fe93f4ac0c6bc7485f9e7473c4b8c4a33ec12199c0", | |||
key | |||
); | |||
}); | |||
}); | |||
it("auto generated test 9", function() { | |||
return pbkdf2("password", "lesspass", 8192, 32, "sha256").then(function( | |||
key | |||
) { | |||
assert.equal( | |||
"7d05ee25597dcc3ac16d082aa910e7707f75be620ed8db5bef7245e2a8579116", | |||
key | |||
); | |||
}); | |||
}); | |||
it("auto generated test 10", function() { | |||
return pbkdf2( | |||
"password2", | |||
"contact@lesspass.com", | |||
8192, | |||
32, | |||
"sha256" | |||
).then(function(key) { | |||
assert.equal( | |||
"ce853092fc54fe88c281e38df97bd5826d64e6bee315dc94939cbba8930df0e4", | |||
key | |||
); | |||
}); | |||
}); | |||
}); |
@@ -1 +0,0 @@ | |||
Subproject commit 0b416c5cf19fe912b186c90f250734a987e2a1ac |
@@ -0,0 +1,17 @@ | |||
# editorconfig.org | |||
root = true | |||
[*] | |||
charset = utf-8 | |||
end_of_line = lf | |||
indent_size = 2 | |||
indent_style = space | |||
insert_final_newline = true | |||
trim_trailing_whitespace = true | |||
[*.md] | |||
trim_trailing_whitespace = false | |||
[*.py] | |||
indent_size = 4 |
@@ -0,0 +1 @@ | |||
node_modules |
@@ -0,0 +1,24 @@ | |||
# LessPass cozy | |||
LessPass for MyCozyCloud | |||
## Install | |||
Install app with `https://github.com/lesspass/cozy@master` repository | |||
## Build | |||
npm run build | |||
## Run | |||
npm start | |||
## License | |||
This project is licensed under the terms of the GNU GPLv3. | |||
## Issues | |||
report issues on [LessPass project](https://github.com/lesspass/lesspass/issues) |