Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Surya/core 9290/py triplesec v4 #19

Merged
merged 17 commits into from Dec 4, 2018
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 3 additions & 5 deletions .travis.yml
@@ -1,17 +1,15 @@
language:
python
python:
- 2.6
- 2.7
- 3.3
- 3.6
install:
- pip install -e .
- if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install unittest2; fi
- pip install -r requirements.txt
script:
nosetests --verbose
notifications:
email:
- filippo.valsorda@gmail.com
- max@keybase.io
after_success:
- pip install coveralls
- coverage run --source=triplesec setup.py -q nosetests
Expand Down
17 changes: 14 additions & 3 deletions README.rst
Expand Up @@ -74,11 +74,16 @@ Here is the help::
encoded; hex encode all output
-k KEY, --key KEY the TripleSec key; if not specified will check the
TRIPLESEC_KEY env variable, then prompt the user for it
--keccak-compatibility Use Keccak instead of SHA3 for the second MAC. Only effective in versions before 4.

API
---
Changes in 0.5
-----------------------
For message authentication, the Triplesec spec uses the Keccak SHA3 proposal function for versions 1 through 3, but for some time, this library used the standardized SHA3-512 function instead. Thus, by default, the Python implementation for versions 1 through 3 is incompatible with the JavaScript and Golang implementations.
From version 4 and going forward, the spec will use only the standardized SHA3-512 function (provided, for example, by `hashlib` in Python), and the Python, JavaScript, and Golang implementations will be compatible.

Sphinx documentation coming soon.
If you would like to use Keccak with versions 1 through 3 (and thus achieve compatibility with the Node and Go packages), you can pass in `keccak_compatibility=True` to `encrypt` and `decrypt`, or on the commandline as detailed in the Example section.

Additionally, invocations that do not specify a version will now use version 4 by default, which is incompatible with previous versions.

Example
-------
Expand All @@ -93,3 +98,9 @@ IT'S A YELLOW SUBMARINE
>>> x = T.encrypt(b"IT'S A YELLOW SUBMARINE")
>>> print(T.decrypt(x).decode())
IT'S A YELLOW SUBMARINE

# Use --keccak-compatibility in the command line to decrypt version 1 through 3 messages made with the Node or Go packages.
$ TRIPLESEC_KEY=abc triplesec dec 1c94d7de000000031355e46727ab2f1a1575a605e4aa5012dcf0e13e55891a4167b10a0f5c173a2e6c6cbb5718f3f7021005f2501b8b5b674bed2553687404aae7aed32d4e9a7bb456dbef209786ee14d974e7899a3d8bacfb7f6705f4abeb307047b1360fa2e5721e5e485361d3a59f426af89d6170fd67feba4ccf6c61157e4a563d1de4ed64d7afff92032bc9c5c9e2c125f9f245acf6683c40f3380b0a762c862859b3651a6a51aa1fdd3887e69eecf46cb60e2f6cf2fcf3d29341b2066dd56bb3f164448b6fa4cf4b1ae9312cb147a667350bdaffdd6c4d31
ERROR: Failed authentication of the data
$ TRIPLESEC_KEY=abc triplesec --keccak-compatibility dec 1c94d7de000000031355e46727ab2f1a1575a605e4aa5012dcf0e13e55891a4167b10a0f5c173a2e6c6cbb5718f3f7021005f2501b8b5b674bed2553687404aae7aed32d4e9a7bb456dbef209786ee14d974e7899a3d8bacfb7f6705f4abeb307047b1360fa2e5721e5e485361d3a59f426af89d6170fd67feba4ccf6c61157e4a563d1de4ed64d7afff92032bc9c5c9e2c125f9f245acf6683c40f3380b0a762c862859b3651a6a51aa1fdd3887e69eecf46cb60e2f6cf2fcf3d29341b2066dd56bb3f164448b6fa4cf4b1ae9312cb147a667350bdaffdd6c4d31
Hello world
6 changes: 6 additions & 0 deletions requirements.txt
@@ -0,0 +1,6 @@
pycryptodome
scrypt
six
pysha3
twofish
salsa20
8 changes: 4 additions & 4 deletions setup.py
Expand Up @@ -26,10 +26,10 @@

setup(
name = 'TripleSec',
version = '0.4',
version = '0.5',
description = 'a Python implementation of TripleSec',
author = 'Filippo Valsorda',
author_email = 'filippo.valsorda@gmail.com',
author = 'Keybase',
author_email = 'max@keybase.io',
url = 'http://github.com/keybase/python-triplesec',
packages = ['triplesec'],
license = 'BSD-new',
Expand All @@ -41,7 +41,7 @@
'Topic :: Security :: Cryptography',
'Topic :: Software Development :: Libraries'],
long_description = open('README.rst').read(),
install_requires = ["pycrypto",
install_requires = ["pycryptodome",
"scrypt",
"six",
"pysha3",
Expand Down
2 changes: 1 addition & 1 deletion tox.ini
@@ -1,5 +1,5 @@
[tox]
envlist = py26,py27,py33
envlist = py27,py36

[testenv]
deps =
Expand Down
24 changes: 12 additions & 12 deletions triplesec/__init__.py
Expand Up @@ -26,16 +26,13 @@
_constant_time_compare,
win32_utf8_argv
)
from .versions import VERSIONS
from .versions import get_version, valid_version, LATEST_VERSION


### MAIN CLASS
class TripleSec():
LATEST_VERSION = 3
MAGIC_BYTES = MAGIC_BYTES

VERSIONS = VERSIONS

@staticmethod
def _check_key(key):
if key is None: return
Expand Down Expand Up @@ -111,15 +108,15 @@ def encrypt_ascii(self, data, key=None, v=None, extra_bytes=0,
result = digestor(binary_result)
return result

def encrypt(self, data, key=None, v=None, extra_bytes=0):
def encrypt(self, data, key=None, v=None, extra_bytes=0, keccak_compatibility=False):
self._check_data(data)
self._check_key(key)
if key is None and self.key is None:
raise TripleSecError(u"You didn't initialize TripleSec with a key, so you need to specify one")
if key is None: key = self.key

if not v: v = self.LATEST_VERSION
version = self.VERSIONS[v]
if not v: v = LATEST_VERSION
version = get_version(v, keccak_compatibility)
result, extra = self._encrypt(data, key, version, extra_bytes)

self._check_output_type(result)
Expand Down Expand Up @@ -180,7 +177,7 @@ def decrypt_ascii(self, ascii_string, key=None, digest="hex"):
result = self.decrypt(binary_string, key)
return result

def decrypt(self, data, key=None):
def decrypt(self, data, key=None, keccak_compatibility=False):
self._check_data(data)
self._check_key(key)
if key is None and self.key is None:
Expand All @@ -191,10 +188,10 @@ def decrypt(self, data, key=None):
raise TripleSecError(u"This does not look like a TripleSec ciphertext")

header_version = struct.unpack(">I", data[4:8])[0]
if header_version not in self.VERSIONS:
if not valid_version(header_version):
raise TripleSecError(u"Unimplemented version: " + str(header_version))

version = self.VERSIONS[header_version]
version = get_version(header_version, keccak_compatibility)
result = self._decrypt(data, key, version)

self._check_output_type(result)
Expand Down Expand Up @@ -284,6 +281,9 @@ def main():
help="consider all input (key, plaintext, ciphertext) to be hex encoded; "
"hex encode all output")

parser.add_argument('--keccak-compatibility', action='store_true',
help="Use Keccak instead of SHA3 for the second MAC. Only effective in versions before 4.")

parser.add_argument('-k', '--key', help="the TripleSec key; "
"if not specified will check the TRIPLESEC_KEY env variable, "
"then prompt the user for it")
Expand Down Expand Up @@ -356,7 +356,7 @@ def main():
try:
if args._command == 'dec':
ciphertext = data if args.binary else binascii.unhexlify(data.strip())
plaintext = decrypt(ciphertext, key)
plaintext = decrypt(ciphertext, key, args.keccak_compatibility)
if args.binary:
getattr(sys.stdout, 'buffer', sys.stdout).write(plaintext)
elif args.hex:
Expand All @@ -366,7 +366,7 @@ def main():

elif args._command == 'enc':
plaintext = data
ciphertext = encrypt(plaintext, key)
ciphertext = encrypt(plaintext, key, args.keccak_compatibility)
stdout = getattr(sys.stdout, 'buffer', sys.stdout)
stdout.write(ciphertext if args.binary else binascii.hexlify(ciphertext) + b'\n')

Expand Down
32 changes: 30 additions & 2 deletions triplesec/crypto.py
Expand Up @@ -24,14 +24,34 @@
from .utils import (
TripleSecFailedAssertion,
TripleSecError,
sha3_512
sha3_512,
keccak
)

def validate_key_size(key, key_size, algorithm):
if len(key) != key_size:
raise TripleSecFailedAssertion(u"Wrong {algo} key size"
.format(algo=algorithm))

def check_and_increment_counter(ctr):
# This function is adapted from pycryptodome's source code at
# https://github.com/Legrandin/pycryptodome/blob/39626a5b01ce5c1cf51d022be166ad0aea722177/lib/Crypto/Cipher/_mode_ctr.py#L366
counter_len = ctr["counter_len"]
prefix = ctr["prefix"]
suffix = ctr["suffix"]
initial_value = ctr["initial_value"]
little_endian = ctr["little_endian"]
words = []
while initial_value > 0:
words.append(struct.pack('B', initial_value & 255))
initial_value >>= 8
words += [ b'\x00' ] * max(0, counter_len - len(words))
if not little_endian:
words.reverse()
counter_block = prefix + b"".join(words) + suffix
ctr["initial_value"] += 1
return counter_block

class BlockCipher(object):

@classmethod
Expand Down Expand Up @@ -86,7 +106,7 @@ def _gen_keystream(cls, length, tfish, ctr):
req_blocks = length // cls.block_size + 1
keystream = b''
for _ in range(req_blocks):
keystream += tfish.encrypt(ctr())
keystream += tfish.encrypt(check_and_increment_counter(ctr))
return keystream[:length]

@classmethod
Expand Down Expand Up @@ -136,6 +156,9 @@ def HMAC_SHA512(data, key):
def HMAC_SHA3(data, key):
return hmac.new(key, data, sha3_512).digest()

def HMAC_KECCAK(data, key):
return hmac.new(key, data, keccak).digest()

def Scrypt(key, salt, length, parameters):
try:
return scrypt.hash(key, salt, parameters.N, parameters.r, parameters.p, length)
Expand All @@ -147,6 +170,11 @@ def XOR_HMAC_SHA3_SHA512(data, key):
h1 = struct.pack(">I", 1)
return strxor(HMAC_SHA512(h0 + data, key), HMAC_SHA3(h1 + data, key))

def XOR_HMAC_KECCAK_SHA512(data, key):
h0 = struct.pack(">I", 0)
h1 = struct.pack(">I", 1)
return strxor(HMAC_SHA512(h0 + data, key), HMAC_KECCAK(h1 + data, key))

def PBKDF2(key, salt, length, parameters):
prf = lambda key, msg: parameters.PRF(msg, key) # Our convention is different
return Crypto_PBKDF2(key, salt, length, parameters.i, prf)
16 changes: 3 additions & 13 deletions triplesec/test/test.py
Expand Up @@ -24,6 +24,7 @@

# A generic vector for various tests
VECTOR = vectors[0]
assert 'disabled' not in VECTOR


class TripleSec_tests(unittest.TestCase):
Expand Down Expand Up @@ -99,7 +100,7 @@ def test_extra_bytes(self):
self.assertEqual(None, T.extra_bytes())
data = VECTOR['ciphertext']
header_version = struct.unpack(">I", data[4:8])[0]
version = T.VERSIONS[header_version]
version = triplesec.versions.get_version(header_version, False)
header, salt, macs, encrypted_material = T._split_ciphertext(data, version)
mac_keys, cipher_keys, extra = T._key_stretching(VECTOR['key'], salt, version, len(VECTOR['extra']))
self.assertEqual(VECTOR['extra'], extra)
Expand Down Expand Up @@ -127,22 +128,11 @@ def test_tampered_data(self):
c = c[:-2] + six.int2byte(six.indexbytes(c, -2) ^ 25) + six.int2byte(six.indexbytes(c, -1))
self.assertRaisesRegexp(TripleSecError, regex, lambda: triplesec.decrypt(c, VECTOR['key']))

def test_chi_squared(self):
pass # TODO

def test_randomness(self):
pass # TODO

def test_randomness_of_ciphertext(self):
pass # TODO

def test_signatures_v1(self):
inp = unhex('1c94d7de000000019f1d6915ca8035e207292f3f4f88237da9876505dee100dfbda9fd1cd278d3590840109465e5ed347fdeb6fc2ca8c25fa5cf6e317d977f6c5209f46c30055f5c531c')
key = unhex('1ee5eec12cfbf3cc311b855ddfddf913cff40b3a7dce058c4e46b5ba9026ba971a973144cbf180ceca7d35e1600048d414f7d5399b4ae46732c34d898fa68fbb0dbcea10d84201734e83c824d0f66207cf6f1b6a2ba13b9285329707facbc060')
out = unhex('aa761d7d39c1503e3f4601f1e331787dca67794357650d76f6408fb9ea37f9eede1f45fcc741a3ec06e9d23be97eb1fbbcbe64bc6b2c010827469a8a0abbb008b11effefe95ddd558026dd2ce83838d7a087e71d8a98e5cbee59f9f788e99dbe7f9032912a4384af760c56da8d7a40ab057796ded052be17a69a6d14e703a621')

version = TripleSec.VERSIONS[1]

version = triplesec.versions.get_version(1, keccak_compatibility=True)
self.assertEqual(out, b''.join(TripleSec._generate_macs(inp, [key[:48], key[48:]], version)))

def test_ciphers(self):
Expand Down
4 changes: 2 additions & 2 deletions triplesec/test/test_pkbdf2_hmac_sha512_sha3.py
Expand Up @@ -4,15 +4,15 @@
import unittest2 as unittest
else:
import unittest
from triplesec.crypto import PBKDF2, XOR_HMAC_SHA3_SHA512
from triplesec.crypto import PBKDF2, XOR_HMAC_KECCAK_SHA512
from triplesec.utils import PBKDF2_params

class TripleSec_test_PBKDF2(unittest.TestCase):
def test(self):
for i,v in enumerate(test_vectors['vectors']):
key = binascii.unhexlify(v['key'])
salt = binascii.unhexlify(v['salt'])
derived = PBKDF2(key, salt, v['dkLen'], PBKDF2_params(v['c'], XOR_HMAC_SHA3_SHA512))
derived = PBKDF2(key, salt, v['dkLen'], PBKDF2_params(v['c'], XOR_HMAC_KECCAK_SHA512))
self.assertEqual(binascii.hexlify(derived), v['dk'].encode(), repr(v))


Expand Down
9 changes: 9 additions & 0 deletions triplesec/test/vectors.json
@@ -1,9 +1,15 @@
[{
"key": "0xf6b305811712d06e8723",
"ciphertext": "1c94d7de00000004ab62712b6a43fba017a7a13333b59d3650365fcebded3bd64741a99b2070fa12e4145766afa1b2dbcaca4d2053963f441be82963046766f16a4f82186a9ba7a7cf04f19da9a695a4a7ae9f6036a5d3b456ad97d512af55e61245c9a096db8a4b73cc64491ec67e5381ec14f2ce6f3db922b5cec7cea86305681a0204d6fb9522e7ec8851f8d85c4e4319473c2899ece487324093f144d27ea13355fd9a03a0765afc5f5750152824c6632dfd50bd25ac340aaa6e6cd3664c21d4501ef8b3107c3fa62b9a97d79a9ccc64",
"plaintext": "Hello! I am ASCII!"
}, {
"disabled": "not compatible with v3",
"comment": "to generate this, do `iced dev/enc.iced` in the ICS triplesc REPO",
"key": "0x74686973206265207468652070617373776f7264",
"ciphertext": "1c94d7de00000003bd202d905238954eab386b1c8500de93847378e0791793d0c31625f9a7cf7af6ed75abaa248edabe103408ce65a8ada16186a8d08982b82397b59250c7e40b4db3e0f3e4abd4a351fc71799dd23b2c2027d45a311019cc5bcdcbf1978b068e107f53d26aa92c0ff00707754f3e31084fc2a1923c2733f72eb6bafd88784eb8e8b9a30b9f9e049be390c8dd24981ccbeaa448198494c662db397ff561182c25a1c62b279984d4cf3528ddf9215aa1a7acbbc83a2ef868d902593491dd34bf397d06c3bbecafa9eaacd9861b4ffd54fd86d7a69369646a25d2ba12afb80ca43026fd146b1d018bbb8e93f4e5e35f7e10bfe5ba5c7ee3ae5828a47902e0abd6bfdd3599c752f59aa2a3076da38ebe33818c96d5df3476918ec5d218da3cda2aff760ccf4f28e8fe5cc55f50fc1b7abf58039425303d6da2de01a355fa3fc54d285cc194b53e53b82063e3ee0e6d04ef727fda6312986c53067341a33d89ff1fafcfe04688f3e4dea13604c6c2bc3ef7a0cd9e416ced1e2d1a25",
"plaintext": "this be the secret message -> this be the secret message -> this be the secret message -> this be the secret message -> this be the secret message -> this be the secret message"
}, {
"disabled": "not compatible with v3",
"comment": "another shorter vector generate with the iced code",
"key": "ANNA",
"ciphertext": "1c94d7de00000003d0c87b785a9daf6d25776df2d3f9a5d9a0b08e186ecebc2dc20c02a077ecde9e7a6f4cf705c45729d1dbd8f07acf94b6d756336265991b209ee94c57059699b06846506a043837463f594eb922660ee48f5c2a4de14ee4de70d5004668a84e396e2123a8a7de9fde35ccbcb58fdbb5b624cb67adfac29a9c0ccb67e09675da2bdf1cb47646822bfd5ac1e0887cc23e5e0c4866a9bdab3d4ab4ca394f7e1d0acb6697b477e1feae0d9faf1da42ef49f1a311e5ef7cbe2cf347d7e52d83fc18943cecfb0c310881799cff0",
Expand All @@ -15,9 +21,12 @@
"ciphertext": "1c94d7de000000016ea9b9afa82bb3c8f08ea7bb1b86e57224480ce0f1fc4412316811f75fdaaedbc9442a24222b3f770ac79fa9bd08c93aaa8118333fe4e176ee72262c85b415818626128f31e590e6b50c300dd949a44d6427c2630efeed93352003e4cee9ade95b62725403b9350b1bed594ed59e55d9d63396e24a21be42b52996f73e1bbcdbd1042914166c4866483f715496ce586a3e7788b9bf0fd4cbf89db4b3b249573e360e3571b0957c07c1474137963465197a3e486ea4be069708431e0e1b38e92eac31",
"plaintext": "=)"
}, {
"disabled": "not compatible with v3",
"comment": "vector with extra key bytes",
"key": "0xaabbccee",
"extra": "ccfa4dc6cb107a3edafd0954c305f06132178b0c8f07e4505db8e7417cc9d8cc",
"plaintext": "0x1100aabbee",
"ciphertext": "1c94d7de00000003b0c835fe415bd9534bbb614952d2b373367d8a0f664ee0d791152f632d15a9aadef9d4ba6cbc1db87d5cee3e5a26b3209f2a653e83eac1c05a9ad10d4b29b465db35326268231f4f085aa2b0977c2cd5a8a80a93bcd1dec495be59a01f79a2e6b14ca4088ebb2a617fee688b1b0765339eaf8719276270c4b2f3c5753fb273df5e257d6caddc026ffec7ab4df4e8a77d124f5cd7ef4b77e2daeb1affb47d5f249c3a7fd27fac639d6c43c648129176b6c664396d7c8130513be61a4028cdfe573012e36c91edf5b661e294d53c"
}]


25 changes: 25 additions & 0 deletions triplesec/utils.py
Expand Up @@ -14,6 +14,7 @@
import sys
from six.moves import zip
from collections import namedtuple
import Crypto.Hash.keccak
if sys.version_info > (3, 2):
if 'sha3_512' not in hashlib.algorithms_available:
import sha3
Expand Down Expand Up @@ -82,6 +83,30 @@ def copy(self):
return copy
sha3_512 = lambda s=b'': new_sha3_512(s)

class new_keccak(object):
block_size = 72
digest_size = 64

def __init__(self, string=b''):
self._obj = Crypto.Hash.keccak.new(digest_bits=512)
self.input = b""
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is storing this okay (used in copy)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Holding onto an empty string?

self.input += string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, you mean this....

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it can be called with some of the MAC key data in the HMAC construction, and therefore might be passing a MAC key around. That ok?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I usually assume that anything in the processes' memory is safe. But I saw a lot of scrubbing in the JS repo, is that necessary or just an extra precaution?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was an extra precaution and likely not ineffective since with interpreted languages, you never really know what happens to memory.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense. I got rid of this custom stuff entirely. And I finally figured out what caused the sudden change: pysha3 upgraded to 1.0 and changed the default sha3 function, but we weren't version locked so tests started failing (which I just now did). Still using the library since it provides keccak which the python stdlib doesn't.

self._obj.update(string)

def digest(self):
return self._obj.digest()

def hexdigest(self):
return self._obj.hexdigest()

def update(self, string):
self.input += string
return self._obj.update(string)

def copy(self):
return new_keccak(self.input)
keccak = lambda s=b'': new_keccak(s)

def win32_utf8_argv():
"""Uses shell32.GetCommandLineArgvW to get sys.argv as a list of UTF-8
strings.
Expand Down