omlish 0.0.0.dev6__py3-none-any.whl → 0.0.0.dev7__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.

Potentially problematic release.


This version of omlish might be problematic. Click here for more details.

Files changed (106) hide show
  1. omlish/__about__.py +109 -5
  2. omlish/__init__.py +0 -8
  3. omlish/asyncs/__init__.py +0 -9
  4. omlish/asyncs/anyio.py +40 -0
  5. omlish/bootstrap.py +737 -0
  6. omlish/check.py +1 -1
  7. omlish/collections/__init__.py +4 -0
  8. omlish/collections/exceptions.py +2 -0
  9. omlish/collections/utils.py +38 -9
  10. omlish/configs/strings.py +2 -0
  11. omlish/dataclasses/__init__.py +7 -0
  12. omlish/dataclasses/impl/descriptors.py +95 -0
  13. omlish/dataclasses/impl/reflect.py +1 -1
  14. omlish/dataclasses/utils.py +23 -0
  15. omlish/{lang/datetimes.py → datetimes.py} +8 -4
  16. omlish/diag/procfs.py +1 -1
  17. omlish/diag/threads.py +131 -48
  18. omlish/docker.py +16 -1
  19. omlish/fnpairs.py +0 -4
  20. omlish/{serde → formats}/dotenv.py +3 -0
  21. omlish/{serde → formats}/yaml.py +2 -2
  22. omlish/graphs/trees.py +1 -1
  23. omlish/http/consts.py +6 -0
  24. omlish/http/sessions.py +2 -2
  25. omlish/inject/__init__.py +4 -0
  26. omlish/inject/binder.py +3 -3
  27. omlish/inject/elements.py +1 -1
  28. omlish/inject/impl/injector.py +57 -27
  29. omlish/inject/impl/origins.py +2 -0
  30. omlish/inject/origins.py +3 -0
  31. omlish/inject/utils.py +18 -0
  32. omlish/iterators.py +69 -2
  33. omlish/lang/__init__.py +16 -7
  34. omlish/lang/classes/restrict.py +10 -0
  35. omlish/lang/contextmanagers.py +1 -1
  36. omlish/lang/descriptors.py +3 -3
  37. omlish/lang/imports.py +67 -0
  38. omlish/lang/iterables.py +40 -0
  39. omlish/lang/maybes.py +3 -0
  40. omlish/lang/objects.py +38 -0
  41. omlish/lang/strings.py +25 -0
  42. omlish/lang/sys.py +9 -0
  43. omlish/lang/typing.py +37 -0
  44. omlish/lite/__init__.py +1 -0
  45. omlish/lite/cached.py +18 -0
  46. omlish/lite/check.py +29 -0
  47. omlish/lite/contextmanagers.py +18 -0
  48. omlish/lite/json.py +30 -0
  49. omlish/lite/logs.py +52 -0
  50. omlish/lite/marshal.py +316 -0
  51. omlish/lite/reflect.py +49 -0
  52. omlish/lite/runtime.py +18 -0
  53. omlish/lite/secrets.py +19 -0
  54. omlish/lite/strings.py +25 -0
  55. omlish/lite/subprocesses.py +112 -0
  56. omlish/logs/configs.py +15 -2
  57. omlish/logs/formatters.py +7 -2
  58. omlish/marshal/__init__.py +28 -0
  59. omlish/marshal/any.py +5 -5
  60. omlish/marshal/base.py +27 -11
  61. omlish/marshal/base64.py +24 -9
  62. omlish/marshal/dataclasses.py +34 -28
  63. omlish/marshal/datetimes.py +74 -18
  64. omlish/marshal/enums.py +14 -8
  65. omlish/marshal/exceptions.py +11 -1
  66. omlish/marshal/factories.py +59 -74
  67. omlish/marshal/forbidden.py +35 -0
  68. omlish/marshal/global_.py +11 -4
  69. omlish/marshal/iterables.py +21 -24
  70. omlish/marshal/mappings.py +23 -26
  71. omlish/marshal/numbers.py +51 -0
  72. omlish/marshal/optionals.py +11 -12
  73. omlish/marshal/polymorphism.py +86 -21
  74. omlish/marshal/primitives.py +4 -5
  75. omlish/marshal/standard.py +13 -8
  76. omlish/marshal/uuids.py +4 -5
  77. omlish/matchfns.py +218 -0
  78. omlish/os.py +64 -0
  79. omlish/reflect/__init__.py +39 -0
  80. omlish/reflect/isinstance.py +38 -0
  81. omlish/reflect/ops.py +84 -0
  82. omlish/reflect/subst.py +110 -0
  83. omlish/reflect/types.py +275 -0
  84. omlish/secrets/__init__.py +18 -2
  85. omlish/secrets/crypto.py +132 -0
  86. omlish/secrets/marshal.py +36 -7
  87. omlish/secrets/openssl.py +207 -0
  88. omlish/secrets/secrets.py +260 -8
  89. omlish/secrets/subprocesses.py +42 -0
  90. omlish/sql/dbs.py +6 -5
  91. omlish/sql/exprs.py +12 -0
  92. omlish/sql/secrets.py +10 -0
  93. omlish/term.py +1 -1
  94. omlish/testing/pytest/plugins/switches.py +54 -19
  95. omlish/text/glyphsplit.py +5 -0
  96. omlish-0.0.0.dev7.dist-info/METADATA +50 -0
  97. {omlish-0.0.0.dev6.dist-info → omlish-0.0.0.dev7.dist-info}/RECORD +104 -76
  98. {omlish-0.0.0.dev6.dist-info → omlish-0.0.0.dev7.dist-info}/WHEEL +1 -1
  99. omlish/reflect.py +0 -470
  100. omlish-0.0.0.dev6.dist-info/METADATA +0 -34
  101. /omlish/{asyncs/futures.py → concurrent.py} +0 -0
  102. /omlish/{serde → formats}/__init__.py +0 -0
  103. /omlish/{serde → formats}/json.py +0 -0
  104. /omlish/{serde → formats}/props.py +0 -0
  105. {omlish-0.0.0.dev6.dist-info → omlish-0.0.0.dev7.dist-info}/LICENSE +0 -0
  106. {omlish-0.0.0.dev6.dist-info → omlish-0.0.0.dev7.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,275 @@
1
+ """
2
+ TODO:
3
+ - visitor / transformer
4
+ - uniform collection isinstance - items() for mappings, iter() for other
5
+ - also check instance type in isinstance not just items lol
6
+ - ta.Generic in mro causing trouble - omit? no longer 1:1
7
+ - cache this shit, esp generic_mro shit
8
+ - cache __hash__ in Generic/Union
9
+ """
10
+ import dataclasses as dc
11
+ import types
12
+ import typing as ta
13
+
14
+
15
+ _NoneType = types.NoneType # type: ignore
16
+
17
+ _NONE_TYPE_FROZENSET: frozenset['Type'] = frozenset([_NoneType])
18
+
19
+
20
+ _GenericAlias = ta._GenericAlias # type: ignore # noqa
21
+ _CallableGenericAlias = ta._CallableGenericAlias # type: ignore # noqa
22
+ _SpecialGenericAlias = ta._SpecialGenericAlias # type: ignore # noqa
23
+ _UnionGenericAlias = ta._UnionGenericAlias # type: ignore # noqa
24
+ _AnnotatedAlias = ta._AnnotatedAlias # type: ignore # noqa
25
+
26
+
27
+ ##
28
+
29
+
30
+ @dc.dataclass(frozen=True)
31
+ class _Special:
32
+ name: str
33
+ alias: _SpecialGenericAlias # type: ignore
34
+ origin: type
35
+ nparams: int
36
+
37
+
38
+ _KNOWN_SPECIALS = [
39
+ _Special(
40
+ v._name, # noqa
41
+ v,
42
+ v.__origin__,
43
+ v._nparams, # noqa
44
+ )
45
+ for v in ta.__dict__.values()
46
+ if isinstance(v, _SpecialGenericAlias)
47
+ ]
48
+
49
+ _KNOWN_SPECIALS_BY_NAME = {s.name: s for s in _KNOWN_SPECIALS}
50
+ _KNOWN_SPECIALS_BY_ALIAS = {s.alias: s for s in _KNOWN_SPECIALS}
51
+ _KNOWN_SPECIALS_BY_ORIGIN = {s.origin: s for s in _KNOWN_SPECIALS}
52
+
53
+ _MAX_KNOWN_SPECIAL_TYPE_VARS = 16
54
+
55
+ _KNOWN_SPECIAL_TYPE_VARS = tuple(
56
+ ta.TypeVar(f'_{i}') # noqa
57
+ for i in range(_MAX_KNOWN_SPECIAL_TYPE_VARS)
58
+ )
59
+
60
+
61
+ ##
62
+
63
+
64
+ def get_params(obj: ta.Any) -> tuple[ta.TypeVar, ...]:
65
+ if isinstance(obj, type):
66
+ if issubclass(obj, ta.Generic): # type: ignore
67
+ return obj.__dict__.get('__parameters__', ()) # noqa
68
+
69
+ if (ks := _KNOWN_SPECIALS_BY_ORIGIN.get(obj)) is not None:
70
+ return _KNOWN_SPECIAL_TYPE_VARS[:ks.nparams]
71
+
72
+ oty = type(obj)
73
+
74
+ if (
75
+ oty is _GenericAlias or
76
+ oty is ta.GenericAlias # type: ignore # noqa
77
+ ):
78
+ return obj.__dict__.get('__parameters__', ()) # noqa
79
+
80
+ if oty is _CallableGenericAlias:
81
+ raise NotImplementedError('get_params not yet implemented for typing.Callable')
82
+
83
+ raise TypeError(obj)
84
+
85
+
86
+ def is_union_type(cls: ta.Any) -> bool:
87
+ if hasattr(ta, 'UnionType'):
88
+ return ta.get_origin(cls) in {ta.Union, getattr(ta, 'UnionType')}
89
+
90
+ else:
91
+ return ta.get_origin(cls) in {ta.Union}
92
+
93
+
94
+ def get_orig_class(obj: ta.Any) -> ta.Any:
95
+ return obj.__orig_class__ # noqa
96
+
97
+
98
+ ##
99
+
100
+
101
+ Type: ta.TypeAlias = ta.Union[
102
+ type,
103
+ ta.TypeVar,
104
+ 'Union',
105
+ 'Generic',
106
+ 'NewType',
107
+ 'Annotated',
108
+ 'Any',
109
+ ]
110
+
111
+
112
+ @dc.dataclass(frozen=True)
113
+ class Union:
114
+ args: frozenset[Type]
115
+
116
+ @property
117
+ def is_optional(self) -> bool:
118
+ return _NoneType in self.args
119
+
120
+ def without_none(self) -> Type:
121
+ if _NoneType not in self.args:
122
+ return self
123
+ rem = self.args - _NONE_TYPE_FROZENSET
124
+ if len(rem) == 1:
125
+ return next(iter(rem))
126
+ return Union(rem)
127
+
128
+
129
+ @dc.dataclass(frozen=True)
130
+ class Generic:
131
+ cls: type
132
+ args: tuple[Type, ...] # map[int, V] = (int, V) | map[T, T] = (T, T)
133
+
134
+ params: tuple[ta.TypeVar, ...] = dc.field(compare=False, repr=False) # map[int, V] = (_0, _1) | map[T, T] = (_0, _1) # noqa
135
+ # params2: tuple[ta.TypeVar, ...] # map[int, V] = (V,) | map[T, T] = (T,)
136
+
137
+ obj: ta.Any = dc.field(compare=False, repr=False)
138
+
139
+ # def __post_init__(self) -> None:
140
+ # if not isinstance(self.cls, type):
141
+ # raise TypeError(self.cls)
142
+
143
+ def full_eq(self, other: 'Generic') -> bool:
144
+ return (
145
+ self.cls == other.cls and
146
+ self.args == other.args and
147
+ self.params == other.params and
148
+ self.obj == other.obj
149
+ )
150
+
151
+
152
+ @dc.dataclass(frozen=True)
153
+ class NewType:
154
+ obj: ta.Any
155
+
156
+
157
+ @dc.dataclass(frozen=True)
158
+ class Annotated:
159
+ ty: Type
160
+ md: ta.Sequence[ta.Any]
161
+
162
+ obj: ta.Any = dc.field(compare=False, repr=False)
163
+
164
+
165
+ class Any:
166
+ pass
167
+
168
+
169
+ ANY = Any()
170
+
171
+
172
+ TYPES: tuple[type, ...] = (
173
+ type,
174
+ ta.TypeVar,
175
+ Union,
176
+ Generic,
177
+ NewType,
178
+ Annotated,
179
+ Any,
180
+ )
181
+
182
+
183
+ ##
184
+
185
+
186
+ def is_type(obj: ta.Any) -> bool:
187
+ if isinstance(obj, (Union, Generic, ta.TypeVar, NewType, Any)): # noqa
188
+ return True
189
+
190
+ oty = type(obj)
191
+
192
+ return (
193
+ oty is _UnionGenericAlias or oty is types.UnionType or # noqa
194
+
195
+ isinstance(obj, ta.NewType) or # noqa
196
+
197
+ (
198
+ oty is _GenericAlias or
199
+ oty is ta.GenericAlias or # type: ignore # noqa
200
+ oty is _CallableGenericAlias
201
+ ) or
202
+
203
+ isinstance(obj, type) or
204
+
205
+ isinstance(obj, _SpecialGenericAlias)
206
+ )
207
+
208
+
209
+ def type_(obj: ta.Any) -> Type:
210
+ if obj is ta.Any:
211
+ return ANY
212
+
213
+ if isinstance(obj, (Union, Generic, ta.TypeVar, NewType, Any)): # noqa
214
+ return obj
215
+
216
+ oty = type(obj)
217
+
218
+ if oty is _UnionGenericAlias or oty is types.UnionType:
219
+ return Union(frozenset(type_(a) for a in ta.get_args(obj)))
220
+
221
+ if isinstance(obj, ta.NewType): # noqa
222
+ return NewType(obj)
223
+
224
+ if (
225
+ oty is _GenericAlias or
226
+ oty is ta.GenericAlias or # type: ignore # noqa
227
+ oty is _CallableGenericAlias
228
+ ):
229
+ origin = ta.get_origin(obj)
230
+ args = ta.get_args(obj)
231
+ if oty is _CallableGenericAlias:
232
+ p, r = args
233
+ if p is Ellipsis or isinstance(p, ta.ParamSpec):
234
+ raise TypeError(f'Callable argument not yet supported for {obj=}')
235
+ args = (*p, r)
236
+ params = _KNOWN_SPECIAL_TYPE_VARS[:len(args)]
237
+ elif origin is ta.Generic:
238
+ params = args
239
+ else:
240
+ params = get_params(origin)
241
+ if len(args) != len(params):
242
+ raise TypeError(f'Mismatched {args=} and {params=} for {obj=}')
243
+ return Generic(
244
+ origin,
245
+ tuple(type_(a) for a in args),
246
+ params,
247
+ obj,
248
+ )
249
+
250
+ if isinstance(obj, type):
251
+ if issubclass(obj, ta.Generic): # type: ignore
252
+ params = get_params(obj)
253
+ return Generic(
254
+ obj,
255
+ params,
256
+ params,
257
+ obj,
258
+ )
259
+ return obj
260
+
261
+ if isinstance(obj, _SpecialGenericAlias):
262
+ if (ks := _KNOWN_SPECIALS_BY_ALIAS.get(obj)) is not None:
263
+ params = _KNOWN_SPECIAL_TYPE_VARS[:ks.nparams]
264
+ return Generic(
265
+ ks.origin,
266
+ params,
267
+ params,
268
+ obj,
269
+ )
270
+
271
+ if isinstance(obj, _AnnotatedAlias):
272
+ o = ta.get_args(obj)[0]
273
+ return Annotated(type_(o), md=obj.__metadata__, obj=obj)
274
+
275
+ raise TypeError(obj)
@@ -1,7 +1,23 @@
1
1
  from .secrets import ( # noqa
2
+ CachingSecrets,
3
+ CompositeSecrets,
2
4
  EMPTY_SECRETS,
3
5
  EmptySecrets,
4
- Secret,
6
+ EnvVarSecrets,
7
+ FnSecrets,
8
+ LoggingSecrets,
9
+ MappingSecrets,
10
+ SecretRef,
11
+ SecretRefOrStr,
5
12
  Secrets,
6
- SimpleSecrets,
13
+ secret_field,
14
+ secret_repr,
7
15
  )
16
+
17
+
18
+ ##
19
+
20
+
21
+ from ..lang.imports import _register_conditional_import # noqa
22
+
23
+ _register_conditional_import('..marshal', '.marshal', __package__)
@@ -0,0 +1,132 @@
1
+ """
2
+ TODO:
3
+ - cryptography vs pycryptodome[x]
4
+ - standardize failure exception
5
+ - chains - take first
6
+ - keysets
7
+
8
+ See:
9
+ - https://soatok.blog/2020/05/13/why-aes-gcm-sucks/
10
+ - https://pycryptodome.readthedocs.io/en/latest/src/cipher/chacha20_poly1305.html
11
+
12
+ ==
13
+
14
+ https://www.tecmint.com/gpg-encrypt-decrypt-files/
15
+
16
+ ==
17
+
18
+ gpg --batch --passphrase '' --quick-gen-key wrmsr default default
19
+ gpg --batch --passphrase '' --quick-gen-key wrmsr2 default default
20
+ echo 'hi there' > secret.txt
21
+ gpg -e -u wrmsr -r wrmsr2 secret.txt
22
+ gpg -d -o secret2.txt secret.txt.gpg
23
+
24
+ gpg --batch -c --passphrase-file /var/secret.key -o some.gpg toencrypt.txt
25
+
26
+ openssl rand -rand /dev/urandom 128 > barf.key
27
+ openssl enc -in secret.txt -out secret.txt.enc -e -aes256 -pbkdf2 -kfile barf.key
28
+ openssl aes-256-cbc -d -pbkdf2 -in secret.txt.enc -out secret3.txt -kfile barf.key
29
+
30
+ https://wiki.openssl.org/index.php/Enc#Options
31
+ -pass 'file:...'
32
+ """
33
+ import abc
34
+ import secrets
35
+ import typing as ta
36
+
37
+ from .. import lang
38
+
39
+
40
+ if ta.TYPE_CHECKING:
41
+ from cryptography import exceptions as cry_exc
42
+ from cryptography import fernet as cry_fernet
43
+ from cryptography.hazmat.primitives.ciphers import aead as cry_aead
44
+ else:
45
+ cry_aead = lang.proxy_import('cryptography.hazmat.primitives.ciphers.aead')
46
+ cry_exc = lang.proxy_import('cryptography.exceptions')
47
+ cry_fernet = lang.proxy_import('cryptography.fernet')
48
+
49
+
50
+ ##
51
+
52
+
53
+ class EncryptionError(Exception):
54
+ pass
55
+
56
+
57
+ class DecryptionError(Exception):
58
+ pass
59
+
60
+
61
+ class Crypto(abc.ABC):
62
+ @abc.abstractmethod
63
+ def generate_key(self) -> bytes:
64
+ raise NotImplementedError
65
+
66
+ @abc.abstractmethod
67
+ def encrypt(self, data: bytes, key: bytes) -> bytes:
68
+ raise NotImplementedError
69
+
70
+ @abc.abstractmethod
71
+ def decrypt(self, data: bytes, key: bytes) -> bytes:
72
+ raise NotImplementedError
73
+
74
+
75
+ ##
76
+
77
+
78
+ class FernetCrypto(Crypto):
79
+
80
+ def generate_key(self) -> bytes:
81
+ return cry_fernet.Fernet.generate_key()
82
+
83
+ def encrypt(self, data: bytes, key: bytes) -> bytes:
84
+ try:
85
+ f = cry_fernet.Fernet(key)
86
+ except ValueError as e:
87
+ raise EncryptionError from e
88
+ return f.encrypt(data)
89
+
90
+ def decrypt(self, data: bytes, key: bytes) -> bytes:
91
+ try:
92
+ f = cry_fernet.Fernet(key)
93
+ except ValueError as e:
94
+ raise DecryptionError from e
95
+ try:
96
+ return f.decrypt(data)
97
+ except cry_fernet.InvalidToken as e:
98
+ raise DecryptionError from e
99
+
100
+
101
+ class AesgsmCrypto(Crypto):
102
+ """https://stackoverflow.com/a/59835994"""
103
+
104
+ def generate_key(self) -> bytes:
105
+ return secrets.token_bytes(32)
106
+
107
+ def encrypt(self, data: bytes, key: bytes) -> bytes:
108
+ nonce = secrets.token_bytes(12)
109
+ return nonce + cry_aead.AESGCM(key).encrypt(nonce, data, b'')
110
+
111
+ def decrypt(self, data: bytes, key: bytes) -> bytes:
112
+ try:
113
+ return cry_aead.AESGCM(key).decrypt(data[:12], data[12:], b'')
114
+ except cry_exc.InvalidTag as e:
115
+ raise DecryptionError from e
116
+
117
+
118
+ class Chacha20Poly1305Crypto(Crypto):
119
+ """https://cryptography.io/en/latest/hazmat/primitives/aead/#cryptography.hazmat.primitives.ciphers.aead.ChaCha20Poly1305""" # noqa
120
+
121
+ def generate_key(self) -> bytes:
122
+ return cry_aead.ChaCha20Poly1305.generate_key()
123
+
124
+ def encrypt(self, data: bytes, key: bytes) -> bytes:
125
+ nonce = secrets.token_bytes(12)
126
+ return nonce + cry_aead.ChaCha20Poly1305(key).encrypt(nonce, data, b'')
127
+
128
+ def decrypt(self, data: bytes, key: bytes) -> bytes:
129
+ try:
130
+ return cry_aead.ChaCha20Poly1305(key).decrypt(data[:12], data[12:], b'')
131
+ except cry_exc.InvalidTag as e:
132
+ raise DecryptionError from e
omlish/secrets/marshal.py CHANGED
@@ -1,23 +1,30 @@
1
+ """
2
+ TODO:
3
+ - ensure import order or at least warn or smth lol
4
+ - raise exception on ambiguous 'registered' impls
5
+ """
1
6
  import collections.abc
2
7
  import typing as ta
3
8
 
4
9
  from .. import check
5
10
  from .. import dataclasses as dc
11
+ from .. import lang
6
12
  from .. import marshal as msh
13
+ from .. import reflect as rfl
7
14
  from .secrets import Secret
15
+ from .secrets import SecretRef
16
+ from .secrets import SecretRefOrStr
8
17
 
9
18
 
10
- class StrOrSecretMarshaler(msh.Marshaler):
19
+ class StrOrSecretRefMarshalerUnmarshaler(msh.Marshaler, msh.Unmarshaler):
11
20
  def marshal(self, ctx: msh.MarshalContext, o: ta.Any) -> msh.Value:
12
21
  if isinstance(o, str):
13
22
  return o
14
- elif isinstance(o, Secret):
23
+ elif isinstance(o, SecretRef):
15
24
  return {'secret': o.key}
16
25
  else:
17
26
  raise TypeError(o)
18
27
 
19
-
20
- class StrOrSecretUnmarshaler(msh.Unmarshaler):
21
28
  def unmarshal(self, ctx: msh.UnmarshalContext, v: msh.Value) -> ta.Any:
22
29
  if isinstance(v, str):
23
30
  return v
@@ -25,17 +32,39 @@ class StrOrSecretUnmarshaler(msh.Unmarshaler):
25
32
  [(mk, mv)] = v.items()
26
33
  if mk != 'secret':
27
34
  raise TypeError(v)
28
- return Secret(check.isinstance(mv, str))
35
+ return SecretRef(check.isinstance(mv, str))
29
36
  else:
30
37
  raise TypeError(v)
31
38
 
32
39
 
33
40
  @dc.field_modifier
34
41
  def marshal_secret_field(f: dc.Field) -> dc.Field:
42
+ """Mostly obsolete with auto-registration below."""
43
+
35
44
  return dc.update_field_metadata(f, {
36
45
  msh.FieldMetadata: dc.replace(
37
46
  f.metadata.get(msh.FieldMetadata, msh.FieldMetadata()),
38
- marshaler=StrOrSecretMarshaler(),
39
- unmarshaler=StrOrSecretUnmarshaler(),
47
+ marshaler=StrOrSecretRefMarshalerUnmarshaler(),
48
+ unmarshaler=StrOrSecretRefMarshalerUnmarshaler(),
40
49
  ),
41
50
  })
51
+
52
+
53
+ @lang.cached_function
54
+ def _install_standard_marshalling() -> None:
55
+ msh.STANDARD_MARSHALER_FACTORIES[0:0] = [
56
+ msh.ForbiddenTypeMarshalerFactory({Secret}),
57
+ msh.TypeMapMarshalerFactory({
58
+ rfl.type_(SecretRefOrStr): StrOrSecretRefMarshalerUnmarshaler(),
59
+ }),
60
+ ]
61
+
62
+ msh.STANDARD_UNMARSHALER_FACTORIES[0:0] = [
63
+ msh.ForbiddenTypeUnmarshalerFactory({Secret}),
64
+ msh.TypeMapUnmarshalerFactory({
65
+ rfl.type_(SecretRefOrStr): StrOrSecretRefMarshalerUnmarshaler(),
66
+ }),
67
+ ]
68
+
69
+
70
+ _install_standard_marshalling()
@@ -0,0 +1,207 @@
1
+ """
2
+ TODO:
3
+ - LibreSSL supports aes-256-gcm: https://crypto.stackexchange.com/a/76178
4
+ """
5
+ import hashlib
6
+ import secrets
7
+ import subprocess
8
+ import typing as ta
9
+
10
+ from .. import lang
11
+ from .crypto import Crypto
12
+ from .crypto import DecryptionError
13
+ from .crypto import EncryptionError
14
+ from .subprocesses import SubprocessFileInputMethod
15
+ from .subprocesses import pipe_fd_subprocess_file_input # noqa
16
+ from .subprocesses import temp_subprocess_file_input # noqa
17
+
18
+
19
+ if ta.TYPE_CHECKING:
20
+ from cryptography.hazmat.primitives import ciphers as cry_ciphs
21
+ from cryptography.hazmat.primitives.ciphers import algorithms as cry_algs
22
+ from cryptography.hazmat.primitives.ciphers import modes as cry_modes
23
+
24
+ else:
25
+ cry_ciphs = lang.proxy_import('cryptography.hazmat.primitives.ciphers')
26
+ cry_algs = lang.proxy_import('cryptography.hazmat.primitives.ciphers.algorithms')
27
+ cry_modes = lang.proxy_import('cryptography.hazmat.primitives.ciphers.modes')
28
+
29
+
30
+ ##
31
+
32
+
33
+ DEFAULT_KEY_SIZE = 64
34
+
35
+
36
+ def generate_key(self, sz: int = DEFAULT_KEY_SIZE) -> bytes:
37
+ # !! https://docs.openssl.org/3.0/man7/passphrase-encoding/
38
+ # Must not contain null bytes!
39
+ return secrets.token_hex(sz).encode('ascii')
40
+
41
+
42
+ ##
43
+
44
+
45
+ class OpensslAes265CbcCrypto(Crypto):
46
+ """
47
+ !!! https://docs.openssl.org/3.0/man7/passphrase-encoding/
48
+ https://cryptography.io/en/latest/hazmat/primitives/symmetric-encryption/#cryptography.hazmat.primitives.ciphers.Cipher
49
+ https://stackoverflow.com/questions/16761458/how-to-decrypt-openssl-aes-encrypted-files-in-python
50
+ https://github.com/openssl/openssl/blob/3c1713aeed4dc7d1ac25e9e365b8bd98afead638/apps/enc.c#L555-L573
51
+ """
52
+
53
+ def __init__(
54
+ self,
55
+ *,
56
+ iters: int = 10_000,
57
+ ) -> None:
58
+ super().__init__()
59
+ self._iters = iters
60
+
61
+ def generate_key(self, sz: int = DEFAULT_KEY_SIZE) -> bytes:
62
+ # This actually can handle null bytes, but we don't generate keys with them for compatibility.
63
+ return generate_key(sz)
64
+
65
+ block_size = 16
66
+ key_length = 32
67
+ iv_length = 16
68
+ salt_length = 8
69
+ prefix = b'Salted__'
70
+
71
+ def _make_cipher(self, key: bytes, salt: bytes) -> 'cry_ciphs.Cipher':
72
+ dk = hashlib.pbkdf2_hmac(
73
+ 'sha256',
74
+ key,
75
+ salt,
76
+ self._iters,
77
+ self.key_length + self.iv_length,
78
+ )
79
+
80
+ return cry_ciphs.Cipher(
81
+ cry_algs.AES256(dk[:self.key_length]),
82
+ cry_modes.CBC(dk[self.key_length:]),
83
+ )
84
+
85
+ def encrypt(self, data: bytes, key: bytes, *, salt: bytes | None = None) -> bytes:
86
+ if salt is None:
87
+ salt = secrets.token_bytes(self.salt_length)
88
+ elif len(salt) != self.salt_length:
89
+ raise EncryptionError('bad salt length')
90
+
91
+ last_byte = self.block_size - (len(data) % self.block_size)
92
+ raw = data + bytes([last_byte] * last_byte)
93
+
94
+ cipher = self._make_cipher(key, salt)
95
+
96
+ encryptor = cipher.encryptor()
97
+ enc = encryptor.update(raw) + encryptor.finalize()
98
+
99
+ return b''.join([
100
+ self.prefix,
101
+ salt,
102
+ enc,
103
+ ])
104
+
105
+ def decrypt(self, data: bytes, key: bytes) -> bytes:
106
+ if not data.startswith(self.prefix):
107
+ raise DecryptionError('bad prefix')
108
+
109
+ salt = data[len(self.prefix):self.block_size]
110
+ cipher = self._make_cipher(key, salt)
111
+
112
+ decryptor = cipher.decryptor()
113
+ dec = decryptor.update(data[self.block_size:]) + decryptor.finalize()
114
+
115
+ last_byte = dec[-1]
116
+ if last_byte > self.block_size:
117
+ raise DecryptionError('bad padding')
118
+
119
+ return dec[:-last_byte]
120
+
121
+
122
+ ##
123
+
124
+
125
+ class OpensslSubprocessAes256CbcCrypto(Crypto):
126
+ def __init__(
127
+ self,
128
+ *,
129
+ cmd: ta.Sequence[str] = ('openssl',),
130
+ timeout: float = 5.,
131
+ file_input: SubprocessFileInputMethod = temp_subprocess_file_input,
132
+ ) -> None:
133
+ super().__init__()
134
+ self._cmd = cmd
135
+ self._timeout = timeout
136
+ self._file_input = file_input
137
+
138
+ def generate_key(self, sz: int = DEFAULT_KEY_SIZE) -> bytes:
139
+ return generate_key(sz)
140
+
141
+ def encrypt(self, data: bytes, key: bytes) -> bytes:
142
+ # !! https://docs.openssl.org/3.0/man7/passphrase-encoding/
143
+ # Must not contain null bytes!
144
+ if 0 in key:
145
+ raise Exception('invalid key')
146
+ with self._file_input(key) as fi:
147
+ proc = subprocess.Popen(
148
+ [
149
+ *self._cmd,
150
+ 'aes-256-cbc',
151
+
152
+ '-e',
153
+
154
+ '-pbkdf2',
155
+ '-salt',
156
+ '-iter', '10000',
157
+
158
+ '-in', '-',
159
+ '-out', '-',
160
+ '-kfile', fi.file_path,
161
+ ],
162
+ stdin=subprocess.PIPE,
163
+ stdout=subprocess.PIPE,
164
+ stderr=subprocess.PIPE,
165
+ pass_fds=[*fi.pass_fds],
166
+ )
167
+ out, err = proc.communicate(
168
+ data,
169
+ timeout=self._timeout,
170
+ )
171
+ if proc.returncode != 0:
172
+ raise EncryptionError
173
+ return out
174
+
175
+ def decrypt(self, data: bytes, key: bytes) -> bytes:
176
+ # !! https://docs.openssl.org/3.0/man7/passphrase-encoding/
177
+ # Must not contain null bytes!
178
+ if 0 in key:
179
+ raise Exception('invalid key')
180
+ with self._file_input(key) as fi:
181
+ proc = subprocess.Popen(
182
+ [
183
+ *self._cmd,
184
+ 'aes-256-cbc',
185
+
186
+ '-d',
187
+
188
+ '-pbkdf2',
189
+ '-salt',
190
+ '-iter', '10000',
191
+
192
+ '-in', '-',
193
+ '-out', '-',
194
+ '-kfile', fi.file_path,
195
+ ],
196
+ stdin=subprocess.PIPE,
197
+ stdout=subprocess.PIPE,
198
+ stderr=subprocess.PIPE,
199
+ pass_fds=[*fi.pass_fds],
200
+ )
201
+ out, err = proc.communicate(
202
+ data,
203
+ timeout=self._timeout,
204
+ )
205
+ if proc.returncode != 0:
206
+ raise DecryptionError
207
+ return out