Skip to content

Commit c5f5985

Browse files
committed
Initial commit
0 parents  commit c5f5985

13 files changed

+504
-0
lines changed

.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
syntax: glob
2+
3+
*.pyc
4+
__pycache__

DESCRIPTION.rst

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
OpenSSH Public Key Parser for Python
2+
====================================
3+
4+
This library validates OpenSSH public keys.
5+
6+
::
7+
8+

LICENSE.txt

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
Copyright (c) 2014, Olli Jarva <[email protected]>
2+
All rights reserved.
3+
4+
Redistribution and use in source and binary forms, with or without
5+
modification, are permitted provided that the following conditions are met:
6+
7+
1. Redistributions of source code must retain the above copyright notice,
8+
this list of conditions and the following disclaimer.
9+
10+
2. Redistributions in binary form must reproduce the above copyright notice,
11+
this list of conditions and the following disclaimer in the documentation
12+
and/or other materials provided with the distribution.
13+
14+
3. Neither the name of the copyright holder nor the names of its
15+
contributors may be used to endorse or promote products derived from this
16+
software without specific prior written permission.
17+
18+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21+
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
22+
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23+
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24+
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25+
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26+
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27+
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28+
POSSIBILITY OF SUCH DAMAGE.

README.rst

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
OpenSSH Public Key Parser for Python
2+
====================================
3+
4+
This library validates OpenSSH public keys.
5+
6+
Currently ssh-rsa, ssh-dss (DSA) and ecdsa-ssh keys with NIST curves is supported.
7+
8+
::
9+
10+
import sshpubkeys
11+
ssh = SSHKey("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAYQCxO38tKAJXIs9ivPxt7AY" \
12+
+ "dfybgtAR1ow3Qkb9GPQ6wkFHQqcFDe6faKCxH6iDRteo4D8L8B" \
13+
+ "xwzN42uZSB0nfmjkIxFTcEU3mFSXEbWByg78aoddMrAAjatyrh" \
14+
+ "H1pON6P0= ojarva@ojar-laptop")
15+
print(ssh.bits) # 768
16+
print(ssh.hash()) # 56:84:1e:90:08:3b:60:c7:29:70:5f:5e:25:a6:3b:86
17+
18+
19+
Exceptions
20+
----------
21+
22+
- NotImplementedError if invalid ecdsa curve or unknown key type is encountered.
23+
- InvalidKeyException if any other error is encountered:
24+
- TooShortKeyException if key is too short (<768 bits for RSA, <1024 for DSA)
25+
- InvalidTypeException if key type ("ssh-rsa" in above example) does not match to what is included in base64 encoded data.
26+
- MalformedDataException if decoding and extracting the data fails.
27+
28+
Tests
29+
-----
30+
31+
See "tests/" folder for unit tests. Use
32+
33+
::
34+
35+
python setup.py test
36+
37+
or
38+
39+
::
40+
41+
python3 setup.py test
42+
43+
to run test suite. If you have keys that are not parsed properly, or malformed keys that raise incorrect exception, please send your *public key* to [email protected], and I'll include it. Alternatively, open issue or make a pull request in github.

requirements.txt

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
ecdsa==0.11
2+
pycrypto==2.6.1

setup.cfg

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[bdist_wheel]
2+
universal=1

setup.py

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from setuptools import setup, find_packages
2+
from codecs import open
3+
from os import path
4+
5+
here = path.abspath(path.dirname(__file__))
6+
7+
with open(path.join(here, 'README.rst'), encoding='utf-8') as f:
8+
long_description = f.read()
9+
10+
setup(
11+
name='sshpubkeys',
12+
version='1.0.1',
13+
description='SSH public key parser',
14+
long_description=long_description,
15+
url='https://github.com/ojarva/sshpubkeys',
16+
author='Olli Jarva',
17+
author_email='[email protected]',
18+
license='BSD',
19+
20+
classifiers=[
21+
'Development Status :: 4 - Beta',
22+
23+
'Intended Audience :: Developers',
24+
'Intended Audience :: System Administrators',
25+
'Topic :: Security',
26+
'License :: OSI Approved :: BSD License',
27+
28+
'Programming Language :: Python :: 2.7',
29+
'Programming Language :: Python :: 3',
30+
'Programming Language :: Python :: 3.2',
31+
'Programming Language :: Python :: 3.3',
32+
'Programming Language :: Python :: 3.4',
33+
],
34+
keywords='ssh pubkey public key openssh ssh-rsa ssh-dss',
35+
packages=["sshpubkeys"],
36+
test_suite="tests",
37+
install_requires=['pycrypto>=2.6', 'ecdsa>=0.11'],
38+
39+
extras_require = {
40+
'dev': ['twine', 'wheel'],
41+
},
42+
)

sshpubkeys/__init__.py

+146
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import base64
2+
import binascii
3+
import hashlib
4+
import struct
5+
import ecdsa
6+
import sys
7+
8+
from Crypto.PublicKey import RSA, DSA
9+
10+
INT_LEN = 4
11+
12+
class InvalidKeyException(Exception):
13+
pass
14+
15+
class TooShortKeyException(InvalidKeyException):
16+
pass
17+
18+
class InvalidTypeException(InvalidKeyException):
19+
pass
20+
21+
class MalformedDataException(InvalidKeyException):
22+
pass
23+
24+
class SSHKey:
25+
def __init__(self, keydata):
26+
self.keydata = keydata
27+
self.current_position = 0
28+
self.decoded_key = None
29+
self.parse()
30+
31+
def hash(self):
32+
""" Calculates fingerprint hash.
33+
34+
Shamelessly copied from http://stackoverflow.com/questions/6682815/deriving-an-ssh-fingerprint-from-a-public-key-in-python
35+
"""
36+
fp_plain = hashlib.md5(self.decoded_key).hexdigest()
37+
return ':'.join(a+b for a, b in zip(fp_plain[::2], fp_plain[1::2]))
38+
39+
def unpack_by_int(self):
40+
""" Returns next data field. """
41+
# Unpack length of data field
42+
try:
43+
requested_data_length = struct.unpack('>I', self.decoded_key[self.current_position:self.current_position+INT_LEN])[0]
44+
except struct.error:
45+
raise MalformedDataException("Unable to unpack %s bytes from the data" % INT_LEN)
46+
47+
# Move pointer to the beginning of the data field
48+
self.current_position += INT_LEN
49+
remaining_data_length = len(self.decoded_key[self.current_position:])
50+
51+
if remaining_data_length < requested_data_length:
52+
raise MalformedDataException("Requested %s bytes, but only %s bytes available." % (requested_data_length, remaining_data_length))
53+
54+
next_data = self.decoded_key[self.current_position:self.current_position+requested_data_length]
55+
# Move pointer to the end of the data field
56+
self.current_position += requested_data_length
57+
return next_data
58+
59+
@classmethod
60+
def parse_long(cls, data):
61+
""" Calculate two's complement """
62+
if sys.version < '3':
63+
ret = long(0)
64+
for byte in data:
65+
ret = (ret << 8) + ord(byte)
66+
return ret
67+
ret = 0
68+
for byte in data:
69+
ret = (ret << 8) + byte
70+
return ret
71+
72+
73+
@classmethod
74+
def split_key(cls, data):
75+
key_parts = data.strip().split(None, 3)
76+
if len(key_parts) < 2: # Key type and content are mandatory fields.
77+
raise InvalidKeyException("Unexpected key format: at least type and base64 encoded value is required")
78+
return key_parts
79+
80+
@classmethod
81+
def decode_key(cls, pubkey_content):
82+
# Decode base64 coded part.
83+
try:
84+
decoded_key = base64.b64decode(pubkey_content.encode("ascii"))
85+
except (TypeError, binascii.Error):
86+
raise InvalidKeyException("Unable to decode the key")
87+
return decoded_key
88+
89+
def parse(self):
90+
self.current_position = 0
91+
key_parts = self.split_key(self.keydata)
92+
93+
key_type = key_parts[0]
94+
pubkey_content = key_parts[1]
95+
96+
self.decoded_key = self.decode_key(pubkey_content)
97+
98+
# Check key type
99+
unpacked_key_type = self.unpack_by_int()
100+
if key_type != unpacked_key_type.decode():
101+
raise InvalidTypeException("Keytype mismatch: %s != %s" % (key_type, unpacked_key_type))
102+
103+
self.key_type = unpacked_key_type
104+
105+
if self.key_type == b"ssh-rsa":
106+
107+
raw_e = self.unpack_by_int()
108+
raw_n = self.unpack_by_int()
109+
110+
unpacked_e = self.parse_long(raw_e)
111+
unpacked_n = self.parse_long(raw_n)
112+
113+
self.rsa = RSA.construct((unpacked_n, unpacked_e))
114+
self.bits = self.rsa.size() + 1
115+
116+
elif self.key_type == b"ssh-dss":
117+
data_fields = {}
118+
for expected_length, item in [(309, "p"), (48, "q"), (309, "g"), (309, "y")]:
119+
data_fields[item] = self.parse_long(self.unpack_by_int())
120+
item_length = len(str(data_fields[item]))
121+
if item_length != expected_length:
122+
raise MalformedDataException("DSA parameter %s has invalid length (%s, expected %s)" % (item, item_length, expected_length))
123+
124+
self.dsa = DSA.construct((data_fields["y"], data_fields["g"], data_fields["p"], data_fields["q"]))
125+
self.bits = self.dsa.size() + 1
126+
if self.bits != 1024:
127+
raise InvalidKeyException("ssh-dss keys must be 1024 bits (was %s)" % self.bits)
128+
129+
elif self.key_type.strip().startswith(b"ecdsa-sha"):
130+
curve_information = self.unpack_by_int()
131+
curve_data = {b"nistp256": (ecdsa.curves.NIST256p, hashlib.sha256),
132+
b"nistp192": (ecdsa.curves.NIST192p, hashlib.sha256),
133+
b"nistp224": (ecdsa.curves.NIST224p, hashlib.sha256),
134+
b"nistp384": (ecdsa.curves.NIST384p, hashlib.sha384),
135+
b"nistp521": (ecdsa.curves.NIST521p, hashlib.sha512)}
136+
if curve_information not in curve_data:
137+
raise NotImplementedError("Invalid curve type: %s" % curve_information)
138+
curve, hash_algorithm = curve_data[curve_information]
139+
140+
data = self.unpack_by_int()
141+
142+
key = ecdsa.VerifyingKey.from_string(data[1:], curve, hash_algorithm)
143+
self.bits = int(curve_information.replace(b"nistp", b"")) # TODO
144+
self.ecdsa = ecdsa
145+
else:
146+
raise NotImplementedError("Invalid key type: %s" % self.key_type)

tests/__init__.py

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
""" Creates tests from lists of both valid and invalid keys.
2+
3+
New test is generated for each key so that running unittests gives out meaningful errors.
4+
5+
"""
6+
7+
import unittest
8+
from sshpubkeys import *
9+
from .test_rsa_keys_lengths import keys as rsa_keys_l
10+
from .test_rsa_keys_failing import keys as rsa_keys_f
11+
from .test_dsa_keys import keys as dsa_keys
12+
from .test_ecdsa_keys import keys as ecdsa_keys
13+
14+
class TestKeys(unittest.TestCase):
15+
def check_key(self, pubkey, bits, fingerprint):
16+
""" Checks valid key """
17+
ssh = SSHKey(pubkey)
18+
self.assertEqual(ssh.bits, bits)
19+
self.assertEqual(ssh.hash(), fingerprint)
20+
21+
def check_fail(self, pubkey, expected_error):
22+
""" Checks that key check raises specified exception """
23+
with self.assertRaises(expected_error):
24+
ssh = SSHKey(pubkey)
25+
26+
def loop_ok(keyset, prefix):
27+
""" Loop over list of valid keys and dynamically create tests """
28+
29+
for i, items in enumerate(keyset):
30+
def ch(pubkey, bits, fingerprint):
31+
return lambda self: self.check_key(pubkey, bits, fingerprint)
32+
prefix_tmp = "%s_%s" % (prefix, i)
33+
if len(items) == 4: # If there is an extra item, use that as test name.
34+
prefix_tmp = items.pop()
35+
pubkey, bits, fingerprint = items
36+
setattr(TestKeys, "test_%s" % prefix_tmp, ch(pubkey, bits, fingerprint))
37+
38+
def loop_failing(keyset, prefix):
39+
""" Loop over list of invalid keys and dynamically create tests """
40+
for i, items in enumerate(keyset):
41+
def ch(pubkey, expected_error):
42+
return lambda self: self.check_fail(pubkey, expected_error)
43+
prefix_tmp = "%s_%s" % (prefix, i)
44+
if len(items) == 3: # If there is an extra item, use that as test name.
45+
prefix_tmp = items.pop()
46+
pubkey, expected_error = items
47+
setattr(TestKeys, "test_%s" % prefix_tmp, ch(pubkey, expected_error))
48+
49+
loop_ok(ecdsa_keys, "ecdsa_ok")
50+
loop_ok(rsa_keys_l, "rsa_ok")
51+
loop_ok(dsa_keys, "dsa_ok")
52+
loop_failing(rsa_keys_f, "rsa_failing")
53+
54+
if __name__ == '__main__':
55+
unittest.main()

tests/test_dsa_keys.py

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
2+
keys = [
3+
["ssh-dss AAAAB3NzaC1kc3MAAACBAPlHIP5sD+T8/Sx1DGEiCzCXqpl7ww40jBg7wTkxu44OH6pNog5PjJt5M4NBULhKva/i+bhIM3ba+H1Or+aHWWFHACV6W2FCGk/k37ApRF8sIa4hsnN0P9qn6VfhbJKee+DBxa21WjjY/MZiljmJz7IQHx5RTxX9I/hJ7cL+aNmrAAAAFQCKteqc4IkgIrjpcpStsxYAhb3MqQAAAIEA+SfIKuTr7QPcinsZQDdmZOXqcg+u9TLzHA4c47y0Kns3T3BVPr9rWdmuh6eImzLO4wMLxLvcg3ecrqFuiCp1IHvXENkGlpB17S+uOXlVDY+sTdXyvYKRKirg5IZefIAP/m08c0QGkhFDbo4ysr9D5gXgH3LB2rMPIAbvMWm/HZQAAACBAKWtAE3hXRQX5KtI4AoIWVTly/6T4JNBt4u24ZRqV7X//CZEZ0cS5YpR/frlpUDI3WKoMtS+VmT3cBFZINashIxZyfBF8+0UX3s34HwNfp0hDW3ZdgZJU56GC2eclMantYGeVrMxgTQd80pxZFgByEhoXGeZaAwUzN8ULo9jHQqM [email protected]", 1024, "76:66:08:8c:86:81:7e:f0:7b:cd:fa:c3:8c:8b:83:c0", "dsa_basic"],
4+
]

tests/test_ecdsa_keys.py

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
keys = [
2+
["ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBE2gqbAChP2h3fTPx3Jy2KdOJUiBGEiqBUwoosfzllw+KrqmGiDEWlufSxdiSOFuLd4a8PSwhoWbdQRVFrZAvFE= joku@vps91201", 256, "7a:16:d1:e9:9d:11:45:a7:7e:64:a0:f0:9b:f1:2e:f3"],
3+
["ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBCCfGmR4U8uiCQ6atu74i19/R3We8vQzcKpvSw/T54lJhIZov3NNLJNnB+BvOV+HvgIwHHjzC95UwWm+YgEsQdZxT2eZOLvPQNw5lOZ4OKjbRmROxyDnF2BptAS/og+rZg== joku@vps91201", 384, "19:f6:7e:f9:da:68:88:4a:bf:1d:4b:07:8a:70:65:f7"],
4+
["ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAF9QpvUneTvt8lu0ePSuzr7iLE9ZMPu2DFTmqh7BVn89IHuQ5dfg9pArxfHZWgu9lMdlOykVx0I6OXkE35A/mFqwwApyiPmiwnojmRnN//pApl6QQFINHzV/PGOSi599F1Y2tHQwcdb44CPOhkUmHtC9wKazSvw/ivbxNjcMzhhHsWGnA== joku@vps91201", 521, "c2:c0:14:36:ad:f8:7e:f1:b3:7f:ad:f2:cd:2a:30:3f"],
5+
]

tests/test_rsa_keys_failing.py

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from sshpubkeys import *
2+
3+
keys = [
4+
["ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAEAgDGrGaNv7i+sGSelzf+7JsCECa9a0sqSg8q4foGkjeV6RkS2tWvKXoT9rICjEdXXodj0CCVhe/V7dmAO0AK8KM0mcvPfTSC8zH1ZBsqaFFTWwmBD01fbH9axrrg3hM0f+AL4bMMWUdxdNrVo90s8PKU6k/HmUNLVx4gC6uQ4A6YczvOVZkuJ4f7HDYK/v1LNTRNeAkw94YpSIZVAoTOZN943+fRCE9cm155pwmFsS+wfzK9+jjhGXNEK0xooiVBRwQM7qetN076vV5FiiM0LO1qYi5JrIqK/70ske86x2mMhMkOe6jqQQbt32PFVmYqYJWcAYXz+bhcQw6oru0c6gNq53aGOnuqI0uh/zV2XH+cN4c8ABcOplzH5YQEUepNVzxylkvpWxdg/ZzR1pvyu5C8RkJWrE3AlCwpix1ak2xTDzgc3rwTTggNSYqvzmYq0mYJhZk2VWsLVxUgdxfwC3LvIHMXSTU9iU2Aqrlhy7bJAqxQFKWy05wsIOI6raPBLqZnPmJ76Ld9aXTrhBFfIDiigr9ZVsVAdOvmyAGCIj4x3Xnlol/3lN0M2+OSV1SU/5ZrS6dIlXPZDak/OXHU0iIFIODhYU5r8EI1M6BI/jsgQ8HatXmOJkfnIkVP0HxD1KvoAFKjVG5sM9KG12LqsnzfD1KL6PzxpOOgoVgznpOjSzVmPKAkU8N/r6R4VIAmZqxpF8Hlzqg/Gfh5kf6CJXXx8OQt1Z/DAsfnl3LvHFNuE8GgXgrUE022W9pV4oONgojc97JSgBXaFkK885UnJKTceAdGQvChEhsU1j3TiyKPox6ICGpoC2nGONJoDE8VQ8dE/YiZmqkZ1lJWX07EwevrIcnz1UBHFaR72aiAADRYClsitLA5+1mnydVstkQ8XQuuKNOFT7miaWUzRHwj9BYGb7oGhNd9oi1VTVjH/5Yq1UiHHESGaIjeLi5uG2KguDFpcvy2ngtUy3ZbvDj+DVOLL+3vAlycRPjN0nBE4e/J6UqdpLg0DbG56zNj86aU0ZgL8kL8NRkFHyV+5zG5iLFkGklbm4nwCxSW/bVT0PFD1is6JbtIk5i+liS+hiuzSF6NGouSuxDy95yWSG8/84fgPDFtvXtOD7Kl4P7EpEAL+VBZnremT9I8tRl1wOHxJKe7jbEcWC2zkuHNlju0Nv5SFijF9c+krRbHDYEzsxPpdqlI4gPtDFdkKwaKN6BrsxBsz9u+PhS1AloUYcxKRqWbqHuDBrKmxnhOgFqJ9ITX0RajtrApt1LfkSBXcFrVEx2nhQkGa6VwjcX/zw2I2iuJFOCQmc9udHIlyaCCSe1PqOIbOlOk5h/Gl1QvRNwSIgf9dZ05lZr6dc+VX8YGdyHsjQ=", InvalidKeyException], # Invalid base64
5+
["ssh-dss AAAAB3NzaC1yc2EAAAADAQABAAAEAgDGrGaNv7i+sGSelzf+7JsCECa9a0sqSg8q4foGkjeV6RkS2tWvKXoT9rICjEdXXodj0CCVhe/V7dmAO0AK8KM0mcvPfTSC8zH1ZBsqaFFTWwmBD01fbH9axrrg3hM0f+AL4bMMWUdxdNrVo90s8PKU6k/HmUNLVx4gC6uQ4A6YczvOVZkuJ4f7HDYK/v1LNTRNeAkw94YpSIZVAoTOZN943+fRCE9cm155pwmFsS+wfzK9+jjhGXNEK0xooiVBRwQM7qetN076vV5FiiM0LO1qYi5JrIqK/70ske86x2mMhMkOe6jqQQbt32PFVmYqYJWcAYXz+bhcQw6oru0c6gNq53aGOnuqI0uh/zV2XH+cN4c8ABcOplzH5YQEUepNVzxylkvpWxdg/ZzR1pvyu5C8RkJWrE3AlCwpix1ak2xTDzgc3rwTTggNSYqvzmYq0mYJhZk2VWsLVxUgdxfwC3LvIHMXSTU9iU2Aqrlhy7bJAqxQFKWy05wsIOI6raPBLqZnPmJ76Ld9aXTrhBFfIDiigr9ZVsVAdOvmyAGCIj4x3Xnlol/3lN0M2+OSV1SU/5ZrS6dIlXPZDak/OXHU0iIFIODhYU5r8EI1M6BI/jsgQ8HatXmOJkfnIkVP0HxD1KvoAFKjVG5sM9KG12LqsnzfD1KL6PzxpOOgoVgznpOjSzVmPKAkU8N/r6R4VIAmZqxpF8Hlzqg/Gfh5kf6CJXXx8OQt1Z/DAsfnl3LvHFNuE8GgXgrUE022W9pV4oONgojc97JSgBXaFkK885UnJKTceAdGQvChEhsU1j3TiyKPox6ICGpoC2nGONJoDE8VQ8dE/YiZmqkZ1lJWX07EwevrIcnz1UBHFaR72aiAADRYClsitLA5+1mnydVstkQ8XQuuKNOFT7miaWUzRHwj9BYGb7oGhNd9oi1VTVjH/5Yq1UiHHESGaIjeLi5uG2KguDFpcvy2ngtUy3ZbvDj+DVOLL+3vAlycRPjN0nBE4e/J6UqdpLg0DbG56zNj86aU0ZgL8kL8NRkFHyV+5zG5iLFkGklbm4nwCxSW/bVT0PFD1is6JbtIk5i+liS+hiuzSF6NGouSuxDy95yWSG8/84fgPDFtvXtOD7Kl4P7EpEAL+VBZnremT9I8tRl1wOHxJKe7jbEcWC2zkuHNlju0Nv5SFijF9c+krRbHDYEzsxPpdqlI4gPtDFdkKwaKN6BrsxBsz9u+PhS1AloUYcxKRqWbqHuDBrKmxnhOgFqJ9ITX0RajtrApt1LfkSBXcFrVEx2nhQkGa6VwjcX/zw2I2iuJFOCQmc9udHIlyaCCSe1PqOIbOlOk5h/Gl1QvRNwSIgf9dZ05lZr6dc+VX8YGdyHsjQ==", InvalidTypeException], # invalid key type
6+
["", InvalidKeyException],
7+
["- -", MalformedDataException],
8+
]
9+

0 commit comments

Comments
 (0)