ciplink 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ciplink/__init__.py ADDED
@@ -0,0 +1,40 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright (c) 2021 Ian Ottoway <ian@ottoway.dev>
4
+ # Copyright (c) 2014 Agostino Ruscito <ruscito@gmail.com>
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ # of this software and associated documentation files (the "Software"), to deal
8
+ # in the Software without restriction, including without limitation the rights
9
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the Software is
11
+ # furnished to do so, subject to the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be included in all
14
+ # copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ # SOFTWARE.
23
+ #
24
+
25
+ from ._version import __version__, __version_info__
26
+ from .logger import *
27
+ from .const import *
28
+ from .tag import Tag
29
+ from .exceptions import *
30
+ from .cip import *
31
+ from .custom_types import *
32
+ from .cip_driver import *
33
+ from .logix_auth import LogixMetadataCredentials
34
+ from .logix_driver import *
35
+ from .slc_driver import *
36
+
37
+ from types import ModuleType as _ModuleType
38
+
39
+ # export everything above except the submodules, so `from ciplink import *` does not shadow map, logger, etc.
40
+ __all__ = [n for n, v in list(globals().items()) if not n.startswith('_') and not isinstance(v, _ModuleType)]
@@ -0,0 +1,108 @@
1
+ """Local credential bundling with RFC 8439 ChaCha20-Poly1305.
2
+
3
+ Only the Python standard library is used. Embedding the reconstruction material in
4
+ the same package provides obfuscation, not secrecy from someone with the code.
5
+ This implementation is scoped to small, locally generated credential bundles.
6
+ """
7
+
8
+ import hmac
9
+ import secrets
10
+ import struct
11
+
12
+
13
+ _HEADER = b"LMK1"
14
+ _MASK = 0xFFFFFFFF
15
+
16
+
17
+ def _rotate(value, count):
18
+ return ((value << count) | (value >> (32 - count))) & _MASK
19
+
20
+
21
+ def _quarter(state, a, b, c, d):
22
+ state[a] = (state[a] + state[b]) & _MASK
23
+ state[d] = _rotate(state[d] ^ state[a], 16)
24
+ state[c] = (state[c] + state[d]) & _MASK
25
+ state[b] = _rotate(state[b] ^ state[c], 12)
26
+ state[a] = (state[a] + state[b]) & _MASK
27
+ state[d] = _rotate(state[d] ^ state[a], 8)
28
+ state[c] = (state[c] + state[d]) & _MASK
29
+ state[b] = _rotate(state[b] ^ state[c], 7)
30
+
31
+
32
+ def _block(key, nonce, counter):
33
+ if not isinstance(key, bytes) or len(key) != 32:
34
+ raise ValueError("Bundle key must contain 32 bytes")
35
+ if not isinstance(nonce, bytes) or len(nonce) != 12 or not 0 <= counter <= _MASK:
36
+ raise ValueError("Invalid bundle nonce or counter")
37
+ initial = list(struct.unpack("<4I", b"expand 32-byte k"))
38
+ initial += list(struct.unpack("<8I", key)) + [counter] + list(struct.unpack("<3I", nonce))
39
+ state = initial[:]
40
+ for _ in range(10):
41
+ for indices in ((0, 4, 8, 12), (1, 5, 9, 13), (2, 6, 10, 14), (3, 7, 11, 15),
42
+ (0, 5, 10, 15), (1, 6, 11, 12), (2, 7, 8, 13), (3, 4, 9, 14)):
43
+ _quarter(state, *indices)
44
+ return struct.pack("<16I", *[(value + original) & _MASK for value, original in zip(state, initial)])
45
+
46
+
47
+ def _crypt(key, nonce, data):
48
+ output = bytearray()
49
+ for counter, offset in enumerate(range(0, len(data), 64), 1):
50
+ stream = _block(key, nonce, counter)
51
+ output.extend(value ^ stream[index] for index, value in enumerate(data[offset:offset + 64]))
52
+ return bytes(output)
53
+
54
+
55
+ def _poly1305(data, key):
56
+ if len(key) != 32:
57
+ raise ValueError("Invalid bundle authenticator key")
58
+ r = int.from_bytes(key[:16], "little") & 0x0FFFFFFC0FFFFFFC0FFFFFFC0FFFFFFF
59
+ s = int.from_bytes(key[16:], "little")
60
+ accumulator = 0
61
+ for offset in range(0, len(data), 16):
62
+ value = int.from_bytes(data[offset:offset + 16] + b"\x01", "little")
63
+ accumulator = (accumulator + value) * r % ((1 << 130) - 5)
64
+ return ((accumulator + s) % (1 << 128)).to_bytes(16, "little")
65
+
66
+
67
+ def _tag(key, nonce, ciphertext, associated):
68
+ mac_data = associated + bytes((-len(associated)) % 16)
69
+ mac_data += ciphertext + bytes((-len(ciphertext)) % 16)
70
+ mac_data += struct.pack("<QQ", len(associated), len(ciphertext))
71
+ return _poly1305(mac_data, _block(key, nonce, 0)[:32])
72
+
73
+
74
+ def _encrypt(key, nonce, plaintext, associated):
75
+ ciphertext = _crypt(key, nonce, plaintext)
76
+ return ciphertext + _tag(key, nonce, ciphertext, associated)
77
+
78
+
79
+ def _decrypt(key, nonce, encrypted, associated):
80
+ if len(encrypted) < 16:
81
+ raise ValueError("Truncated credential bundle")
82
+ ciphertext, tag = encrypted[:-16], encrypted[-16:]
83
+ if not hmac.compare_digest(tag, _tag(key, nonce, ciphertext, associated)):
84
+ raise ValueError("Credential bundle authentication failed")
85
+ return _crypt(key, nonce, ciphertext)
86
+
87
+
88
+ def seal_credentials(certificate, private_key, key):
89
+ if len(certificate) not in (281, 285) or not 1 <= len(private_key) <= 2048:
90
+ raise ValueError("Unsupported credential bundle lengths")
91
+ # Every generated module uses a newly generated key and random nonce.
92
+ nonce = secrets.token_bytes(12)
93
+ payload = struct.pack("<HH", len(certificate), len(private_key)) + certificate + private_key
94
+ return _HEADER + nonce + _encrypt(key, nonce, payload, _HEADER)
95
+
96
+
97
+ def open_credentials(bundle, key):
98
+ if not isinstance(bundle, bytes) or not 36 <= len(bundle) <= 4096 or bundle[:4] != _HEADER:
99
+ raise ValueError("Unsupported encrypted credential bundle")
100
+ payload = _decrypt(key, bundle[4:16], bundle[16:], _HEADER)
101
+ if len(payload) < 4:
102
+ raise ValueError("Truncated credential bundle payload")
103
+ certificate_size, key_size = struct.unpack_from("<HH", payload)
104
+ if certificate_size not in (281, 285) or not 1 <= key_size <= 2048:
105
+ raise ValueError("Unsupported credential bundle payload lengths")
106
+ if len(payload) != 4 + certificate_size + key_size:
107
+ raise ValueError("Credential bundle payload length mismatch")
108
+ return payload[4:4 + certificate_size], payload[4 + certificate_size:]
@@ -0,0 +1,32 @@
1
+ """Generated sealed metadata credentials; reversible local obfuscation."""
2
+ import base64
3
+ from ._logix_bundle_codec import open_credentials
4
+
5
+ _BLOCKS = (
6
+ 'uaqeVqRwHD{(Pv`t8{p18+Ns)I{x!*bl9t_JYMB(J!4Db0yCX9w$~#rtGCaOc2D}LS6-Hc7JA5V?MTy3',
7
+ 'WnlPK!dQ?a@Lc%_Q;;?D#yWb7(`_cz(0LHa`wOD}pi47y(bjL~qRPX{gQ<Sla`V;ESNVb4T5tP+A8@pD',
8
+ 'Zh={^oB%8nb}HGn`Z%`(^hH6*uykD3k7(393&gMx31A4n7Qt}@rTA6_!3;n&aIB`{lWwua3*XPgFH;w&',
9
+ 'lD4?s!V{j7V1L|*^o~*M6#(+<=&Ww#I5s5{NSDR)?X+Znb`R~H_1@KqmImMr_AtDMGW+?C-ul94W0k=W',
10
+ 'HhqnsTL5Ja3Pc()RgO6DnjkPVw)}}L*3TsWcqWsJR$`i(Z+}e9rE2HR_Q(i`XnRDLuz=qTAVw&N`+tFG',
11
+ '#{rU1Xzt#6VS+T&(8haBVoad{NOiGf>6UVQuOw|&j=<i54tpAH57erNlot80>4Slr<uvK*1RyOA)$L?<',
12
+ 'ppCpehcU8oE&z?NdliNMd_5(`G!;YVXZk+aZ3z|sXy_XW+X4qs(HmXV{Czf|LDY|;jrbXOp-GY',
13
+ 'RfWq&wx%QCsGonx{zGhJ>>PaY<ZgK*_)Iy<H{c`fTQFK^v>1>DNA^$kD|){Y><yCaqqXWQ>-%Xu6jFPq',
14
+ 'W|3nui(P|@bfl|YBwmy@;A*~D$!&}Wr|Q1Fcr`R*RG}`zlC{XiecE%qV9lnrz+bfb+9rOM#Y%Kk$Pt`A',
15
+ '1}WV_H_Qr4rsq;qLO&EWm-IxBx*PciRnkQhOM>QGqo{`qL>8N9H*?zLx=uaXBXyMv6U;CEw_oa{;0noX',
16
+ '5QHE~fIbVVJbZ#o56uf0ukhyxvBs{GAa0}aCqyvAY5?C!_sxPb*Tjj`IK(HV%_f~3MCLSmxK24cZ@NTV',
17
+ 'Wk_(8D6U8VB)2zqk}Sw`hicb|#;4(F(T_lF)IRGqI^oW<D@|Ce0D1yf&X3eW&#31(HClM$Q)ymS*RnDv',
18
+ 'Bi3)|iETOD-{H%c8WLbOGDNG~!Jr^u^FC<Jj=V-5h;|B`x`T|#Bn-`WlhR=mu=&x9Z)TbN+w+8md!Htd',
19
+ 'OifEM@QpPieJZHH*^_REvjG(hz}K9sgdV~ul{*}28zy{m0ujl$zE@%>pB{)|Wt^D_*nBT~i+?QqI9^%(',
20
+ 'eRw^ruoMDOPHs4&x{(B3q_!|Xbdzkz@B$BS<0Ppb<2)vvi*NW7K#_=hn38NBmU$a_jx$0kYzYV{*D&<V',
21
+ )
22
+ _ORDER = (13, 5, 9, 11, 12, 8, 1, 14, 3, 4, 7, 2, 10, 0, 6)
23
+ _MATERIAL = ('E-Lv+FnW<bcnV%ny0NhTiye<vXOaO{FaR@DzcZc!', 'CYm@FlgeeOr&|F6RJY7%=n@^Y{z4%`lAmSv`RY+j', 'hzSY`vL^W4+xT?7LCM>9jZcVavq)T>l%~;w<BX@j')
24
+
25
+
26
+ def load():
27
+ data = base64.b85decode("".join(_BLOCKS[index] for index in _ORDER))
28
+ parts = [base64.b85decode(value) for value in _MATERIAL]
29
+ if len(parts) != 3 or any(len(part) != 32 for part in parts):
30
+ raise ValueError("Invalid bundled credential material")
31
+ material = bytes(a ^ b ^ c for a, b, c in zip(*parts))
32
+ return open_credentials(data, material)
ciplink/_version.py ADDED
@@ -0,0 +1,26 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright (c) 2021 Ian Ottoway <ian@ottoway.dev>
4
+ # Copyright (c) 2014 Agostino Ruscito <ruscito@gmail.com>
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ # of this software and associated documentation files (the "Software"), to deal
8
+ # in the Software without restriction, including without limitation the rights
9
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the Software is
11
+ # furnished to do so, subject to the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be included in all
14
+ # copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ # SOFTWARE.
23
+ #
24
+
25
+ __version__ = "0.1.0"
26
+ __version_info__ = tuple(int(x) for x in __version__.split("."))
@@ -0,0 +1,29 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright (c) 2021 Ian Ottoway <ian@ottoway.dev>
4
+ # Copyright (c) 2014 Agostino Ruscito <ruscito@gmail.com>
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ # of this software and associated documentation files (the "Software"), to deal
8
+ # in the Software without restriction, including without limitation the rights
9
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the Software is
11
+ # furnished to do so, subject to the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be included in all
14
+ # copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ # SOFTWARE.
23
+ #
24
+
25
+ from .data_types import *
26
+ from .object_library import *
27
+ from .services import *
28
+ from .status_info import *
29
+ from .pccc import *