From f1f291b270b96fe1511286cb807f02c9741b0d71 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Fri, 9 Feb 2018 13:53:33 +0200 Subject: Rename: to Qvisqve --- qvisqve_secrets/__init__.py | 17 ++++++++ qvisqve_secrets/secrets.py | 90 ++++++++++++++++++++++++++++++++++++++++ qvisqve_secrets/secrets_tests.py | 89 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 196 insertions(+) create mode 100644 qvisqve_secrets/__init__.py create mode 100644 qvisqve_secrets/secrets.py create mode 100644 qvisqve_secrets/secrets_tests.py (limited to 'qvisqve_secrets') diff --git a/qvisqve_secrets/__init__.py b/qvisqve_secrets/__init__.py new file mode 100644 index 0000000..d2eeaf3 --- /dev/null +++ b/qvisqve_secrets/__init__.py @@ -0,0 +1,17 @@ +# Copyright (C) 2017 Lars Wirzenius +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + + +from .secrets import SecretHasher diff --git a/qvisqve_secrets/secrets.py b/qvisqve_secrets/secrets.py new file mode 100644 index 0000000..b469ed8 --- /dev/null +++ b/qvisqve_secrets/secrets.py @@ -0,0 +1,90 @@ +# Copyright (C) 2018 Lars Wirzenius +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + + +import codecs + + +from Cryptodome.Random import get_random_bytes +from Cryptodome.Protocol.KDF import scrypt + + +class SecretHasher: + + # The following have been chosen based on: + # https://www.pycryptodome.org/en/latest/src/protocol/kdf.html + # Corrections, with references, welcome. + _salt_size = 16 + _key_len = 128 + _N = 16384 + _r = 8 + _p = 1 + + _version = 1 + + _ok_args = ['salt', 'key_len', 'N', 'r', 'p'] + + def get_salt(self): + return get_random_bytes(self._salt_size) + + def set_n(self, n): + self._N = n + + def _full_hash(self, cleartext, **kwargs): + assert kwargs == self._filter_kwargs(kwargs) + + hashed_binary = scrypt(cleartext, **kwargs) + params = { + 'version': self._version, + 'hash': self.hex_encode(hashed_binary), + 'salt': self.hex_encode(kwargs['salt']) + } + for key in kwargs: + if key not in params: + params[key] = kwargs[key] + + return params + + def hex_encode(self, byte_string): + return codecs.encode(byte_string, 'hex').decode('ascii') + + def hex_decode(self, hex_string): + return codecs.decode(bytes(hex_string, 'ascii'), 'hex') + + def _filter_kwargs(self, kwargs): + return { + key: kwargs[key] + for key in self._ok_args + } + + def hash(self, cleartext, salt=None): + if salt is None: + salt = self.get_salt() + assert isinstance(salt, bytes) + + kwargs = { + 'salt': salt, + 'key_len': self._key_len, + 'N': self._N, + 'r': self._r, + 'p': self._p, + } + return self._full_hash(cleartext, **kwargs) + + def is_correct(self, hashed, cleartext): + kwargs = self._filter_kwargs(hashed) + salt = self.hex_decode(kwargs.pop('salt')) + h2 = self._full_hash(cleartext, salt=salt, **kwargs) + return hashed == h2 diff --git a/qvisqve_secrets/secrets_tests.py b/qvisqve_secrets/secrets_tests.py new file mode 100644 index 0000000..e7cce87 --- /dev/null +++ b/qvisqve_secrets/secrets_tests.py @@ -0,0 +1,89 @@ +# Copyright (C) 2018 Lars Wirzenius +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + + +import unittest + + +import qvisqve_secrets + + +class SecretHasherTests(unittest.TestCase): + + def test_byte_string_survives_hex_roundtrip(self): + byte_string = b'\x00\x02\x03' + sh = qvisqve_secrets.SecretHasher() + encoded = sh.hex_encode(byte_string) + self.assertEqual(byte_string, sh.hex_decode(encoded)) + + def test_returns_sufficiently_long_salt(self): + sh = qvisqve_secrets.SecretHasher() + salt = sh.get_salt() + self.assertTrue(len(salt) >= 8) + + def test_produces_a_hash(self): + cleartext = 'hunter2' + salt = b'nacl' + sh = qvisqve_secrets.SecretHasher() + hashed = sh.hash(cleartext, salt) + self.assertTrue(isinstance(hashed, dict)) + + def test_produces_a_hash_with_salt(self): + cleartext = 'hunter2' + sh = qvisqve_secrets.SecretHasher() + hashed = sh.hash(cleartext) + self.assertTrue(isinstance(hashed, dict)) + + def test_produces_same_hash_for_same_input(self): + cleartext = 'hunter2' + salt = b'nacl' + sh = qvisqve_secrets.SecretHasher() + hashed1 = sh.hash(cleartext, salt) + hashed2 = sh.hash(cleartext, salt) + self.assertEqual(hashed1, hashed2) + + def test_produces_different_hashes_for_different_cleartext(self): + sh = qvisqve_secrets.SecretHasher() + salt = b'nacl' + hashed1 = sh.hash('hunter2', salt) + hashed2 = sh.hash('swordfish', salt) + self.assertNotEqual(hashed1, hashed2) + + def test_produces_different_hashes_for_same_cleartext(self): + sh = qvisqve_secrets.SecretHasher() + hashed1 = sh.hash('hunter2', b'nacl') + hashed2 = sh.hash('hunter2', b'nh4cl') + self.assertNotEqual(hashed1, hashed2) + + def test_accepts_correct_password(self): + cleartext = 'hunter2' + sh = qvisqve_secrets.SecretHasher() + hashed = sh.hash(cleartext, b'nacl') + self.assertTrue(sh.is_correct(hashed, cleartext)) + + def test_rejects_incorrect_password(self): + cleartext = 'swordfish' + sh = qvisqve_secrets.SecretHasher() + hashed = sh.hash(cleartext, b'nacl') + self.assertTrue(sh.is_correct(hashed, cleartext)) + + def test_handles_parameter_changes(self): + cleartext = 'hunter2' + salt = b'nacl' + sh = qvisqve_secrets.SecretHasher() + hashed = sh.hash(cleartext, salt) + sh.set_n(2**1) + self.assertTrue(sh.is_correct(hashed, cleartext)) + self.assertNotEqual(sh.hash(cleartext, salt), hashed) -- cgit v1.2.1