omlish 0.0.0.dev5__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.
- omlish/__about__.py +109 -5
- omlish/__init__.py +0 -8
- omlish/asyncs/__init__.py +9 -9
- omlish/asyncs/anyio.py +123 -19
- omlish/asyncs/asyncio.py +23 -0
- omlish/asyncs/asyncs.py +9 -6
- omlish/asyncs/bridge.py +316 -0
- omlish/asyncs/trio_asyncio.py +7 -3
- omlish/bootstrap.py +737 -0
- omlish/check.py +1 -1
- omlish/collections/__init__.py +5 -0
- omlish/collections/exceptions.py +2 -0
- omlish/collections/identity.py +7 -0
- omlish/collections/utils.py +38 -9
- omlish/configs/strings.py +96 -0
- omlish/dataclasses/__init__.py +16 -0
- omlish/dataclasses/impl/copy.py +30 -0
- omlish/dataclasses/impl/descriptors.py +95 -0
- omlish/dataclasses/impl/exceptions.py +6 -0
- omlish/dataclasses/impl/fields.py +24 -25
- omlish/dataclasses/impl/init.py +4 -2
- omlish/dataclasses/impl/main.py +2 -0
- omlish/dataclasses/impl/reflect.py +1 -1
- omlish/dataclasses/utils.py +67 -0
- omlish/{lang/datetimes.py → datetimes.py} +8 -4
- omlish/diag/__init__.py +4 -0
- omlish/diag/procfs.py +2 -2
- omlish/{testing → diag}/pydevd.py +35 -0
- omlish/diag/threads.py +131 -48
- omlish/dispatch/_dispatch2.py +65 -0
- omlish/dispatch/_dispatch3.py +104 -0
- omlish/docker.py +16 -1
- omlish/fnpairs.py +11 -4
- omlish/formats/__init__.py +0 -0
- omlish/{configs → formats}/dotenv.py +15 -24
- omlish/{json.py → formats/json.py} +2 -1
- omlish/formats/yaml.py +223 -0
- omlish/graphs/trees.py +1 -1
- omlish/http/asgi.py +2 -1
- omlish/http/collections.py +15 -0
- omlish/http/consts.py +22 -1
- omlish/http/sessions.py +10 -3
- omlish/inject/__init__.py +49 -17
- omlish/inject/binder.py +185 -5
- omlish/inject/bindings.py +3 -36
- omlish/inject/eagers.py +2 -8
- omlish/inject/elements.py +31 -10
- omlish/inject/exceptions.py +1 -1
- omlish/inject/impl/elements.py +37 -12
- omlish/inject/impl/injector.py +72 -25
- omlish/inject/impl/inspect.py +33 -5
- omlish/inject/impl/origins.py +77 -0
- omlish/inject/impl/{private.py → privates.py} +2 -2
- omlish/inject/impl/scopes.py +6 -2
- omlish/inject/injector.py +8 -4
- omlish/inject/inspect.py +18 -0
- omlish/inject/keys.py +8 -14
- omlish/inject/listeners.py +26 -0
- omlish/inject/managed.py +76 -10
- omlish/inject/multis.py +68 -18
- omlish/inject/origins.py +30 -0
- omlish/inject/overrides.py +5 -4
- omlish/inject/{private.py → privates.py} +6 -10
- omlish/inject/providers.py +12 -85
- omlish/inject/scopes.py +13 -6
- omlish/inject/types.py +3 -1
- omlish/inject/utils.py +18 -0
- omlish/iterators.py +69 -2
- omlish/lang/__init__.py +24 -9
- omlish/lang/cached.py +2 -2
- omlish/lang/classes/restrict.py +12 -1
- omlish/lang/classes/simple.py +18 -8
- omlish/lang/contextmanagers.py +13 -4
- omlish/lang/descriptors.py +132 -1
- omlish/lang/functions.py +8 -28
- omlish/lang/imports.py +67 -0
- omlish/lang/iterables.py +60 -1
- omlish/lang/maybes.py +3 -0
- omlish/lang/objects.py +38 -0
- omlish/lang/strings.py +25 -0
- omlish/lang/sys.py +9 -0
- omlish/lang/typing.py +42 -0
- omlish/lifecycles/__init__.py +34 -0
- omlish/lifecycles/abstract.py +43 -0
- omlish/lifecycles/base.py +51 -0
- omlish/lifecycles/contextmanagers.py +74 -0
- omlish/lifecycles/controller.py +116 -0
- omlish/lifecycles/manager.py +161 -0
- omlish/lifecycles/states.py +43 -0
- omlish/lifecycles/transitions.py +64 -0
- omlish/lite/__init__.py +1 -0
- omlish/lite/cached.py +18 -0
- omlish/lite/check.py +29 -0
- omlish/lite/contextmanagers.py +18 -0
- omlish/lite/json.py +30 -0
- omlish/lite/logs.py +52 -0
- omlish/lite/marshal.py +316 -0
- omlish/lite/reflect.py +49 -0
- omlish/lite/runtime.py +18 -0
- omlish/lite/secrets.py +19 -0
- omlish/lite/strings.py +25 -0
- omlish/lite/subprocesses.py +112 -0
- omlish/logs/configs.py +15 -2
- omlish/logs/formatters.py +7 -2
- omlish/marshal/__init__.py +32 -0
- omlish/marshal/any.py +5 -5
- omlish/marshal/base.py +27 -11
- omlish/marshal/base64.py +24 -9
- omlish/marshal/dataclasses.py +34 -28
- omlish/marshal/datetimes.py +74 -18
- omlish/marshal/enums.py +14 -8
- omlish/marshal/exceptions.py +11 -1
- omlish/marshal/factories.py +59 -74
- omlish/marshal/forbidden.py +35 -0
- omlish/marshal/global_.py +11 -4
- omlish/marshal/iterables.py +21 -24
- omlish/marshal/mappings.py +23 -26
- omlish/marshal/naming.py +4 -0
- omlish/marshal/numbers.py +51 -0
- omlish/marshal/objects.py +1 -0
- omlish/marshal/optionals.py +11 -12
- omlish/marshal/polymorphism.py +86 -21
- omlish/marshal/primitives.py +4 -5
- omlish/marshal/standard.py +13 -8
- omlish/marshal/uuids.py +4 -5
- omlish/matchfns.py +218 -0
- omlish/os.py +64 -0
- omlish/reflect/__init__.py +39 -0
- omlish/reflect/isinstance.py +38 -0
- omlish/reflect/ops.py +84 -0
- omlish/reflect/subst.py +110 -0
- omlish/reflect/types.py +275 -0
- omlish/secrets/__init__.py +23 -0
- omlish/secrets/crypto.py +132 -0
- omlish/secrets/marshal.py +70 -0
- omlish/secrets/openssl.py +207 -0
- omlish/secrets/passwords.py +120 -0
- omlish/secrets/secrets.py +299 -0
- omlish/secrets/subprocesses.py +42 -0
- omlish/sql/dbs.py +7 -6
- omlish/sql/duckdb.py +136 -0
- omlish/sql/exprs.py +12 -0
- omlish/sql/secrets.py +10 -0
- omlish/sql/sqlean.py +17 -0
- omlish/term.py +2 -2
- omlish/testing/pytest/__init__.py +3 -2
- omlish/testing/pytest/inject/harness.py +3 -3
- omlish/testing/pytest/marks.py +4 -7
- omlish/testing/pytest/plugins/__init__.py +1 -0
- omlish/testing/pytest/plugins/asyncs.py +136 -0
- omlish/testing/pytest/plugins/pydevd.py +1 -1
- omlish/testing/pytest/plugins/switches.py +54 -19
- omlish/text/glyphsplit.py +97 -0
- omlish-0.0.0.dev7.dist-info/METADATA +50 -0
- omlish-0.0.0.dev7.dist-info/RECORD +268 -0
- {omlish-0.0.0.dev5.dist-info → omlish-0.0.0.dev7.dist-info}/WHEEL +1 -1
- omlish/reflect.py +0 -355
- omlish-0.0.0.dev5.dist-info/METADATA +0 -34
- omlish-0.0.0.dev5.dist-info/RECORD +0 -212
- /omlish/{asyncs/futures.py → concurrent.py} +0 -0
- /omlish/{configs → formats}/props.py +0 -0
- {omlish-0.0.0.dev5.dist-info → omlish-0.0.0.dev7.dist-info}/LICENSE +0 -0
- {omlish-0.0.0.dev5.dist-info → omlish-0.0.0.dev7.dist-info}/top_level.txt +0 -0
omlish/reflect/types.py
ADDED
|
@@ -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)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from .secrets import ( # noqa
|
|
2
|
+
CachingSecrets,
|
|
3
|
+
CompositeSecrets,
|
|
4
|
+
EMPTY_SECRETS,
|
|
5
|
+
EmptySecrets,
|
|
6
|
+
EnvVarSecrets,
|
|
7
|
+
FnSecrets,
|
|
8
|
+
LoggingSecrets,
|
|
9
|
+
MappingSecrets,
|
|
10
|
+
SecretRef,
|
|
11
|
+
SecretRefOrStr,
|
|
12
|
+
Secrets,
|
|
13
|
+
secret_field,
|
|
14
|
+
secret_repr,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
##
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
from ..lang.imports import _register_conditional_import # noqa
|
|
22
|
+
|
|
23
|
+
_register_conditional_import('..marshal', '.marshal', __package__)
|
omlish/secrets/crypto.py
ADDED
|
@@ -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
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TODO:
|
|
3
|
+
- ensure import order or at least warn or smth lol
|
|
4
|
+
- raise exception on ambiguous 'registered' impls
|
|
5
|
+
"""
|
|
6
|
+
import collections.abc
|
|
7
|
+
import typing as ta
|
|
8
|
+
|
|
9
|
+
from .. import check
|
|
10
|
+
from .. import dataclasses as dc
|
|
11
|
+
from .. import lang
|
|
12
|
+
from .. import marshal as msh
|
|
13
|
+
from .. import reflect as rfl
|
|
14
|
+
from .secrets import Secret
|
|
15
|
+
from .secrets import SecretRef
|
|
16
|
+
from .secrets import SecretRefOrStr
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class StrOrSecretRefMarshalerUnmarshaler(msh.Marshaler, msh.Unmarshaler):
|
|
20
|
+
def marshal(self, ctx: msh.MarshalContext, o: ta.Any) -> msh.Value:
|
|
21
|
+
if isinstance(o, str):
|
|
22
|
+
return o
|
|
23
|
+
elif isinstance(o, SecretRef):
|
|
24
|
+
return {'secret': o.key}
|
|
25
|
+
else:
|
|
26
|
+
raise TypeError(o)
|
|
27
|
+
|
|
28
|
+
def unmarshal(self, ctx: msh.UnmarshalContext, v: msh.Value) -> ta.Any:
|
|
29
|
+
if isinstance(v, str):
|
|
30
|
+
return v
|
|
31
|
+
elif isinstance(v, collections.abc.Mapping):
|
|
32
|
+
[(mk, mv)] = v.items()
|
|
33
|
+
if mk != 'secret':
|
|
34
|
+
raise TypeError(v)
|
|
35
|
+
return SecretRef(check.isinstance(mv, str))
|
|
36
|
+
else:
|
|
37
|
+
raise TypeError(v)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dc.field_modifier
|
|
41
|
+
def marshal_secret_field(f: dc.Field) -> dc.Field:
|
|
42
|
+
"""Mostly obsolete with auto-registration below."""
|
|
43
|
+
|
|
44
|
+
return dc.update_field_metadata(f, {
|
|
45
|
+
msh.FieldMetadata: dc.replace(
|
|
46
|
+
f.metadata.get(msh.FieldMetadata, msh.FieldMetadata()),
|
|
47
|
+
marshaler=StrOrSecretRefMarshalerUnmarshaler(),
|
|
48
|
+
unmarshaler=StrOrSecretRefMarshalerUnmarshaler(),
|
|
49
|
+
),
|
|
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
|