-
Notifications
You must be signed in to change notification settings - Fork 73
Expand file tree
/
Copy pathkeeping.py
More file actions
1902 lines (1540 loc) · 78 KB
/
keeping.py
File metadata and controls
1902 lines (1540 loc) · 78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# -*- encoding: utf-8 -*-
"""
KERI
keri.app.keeping module
Terminology:
salt is 128 bit 16 char random bytes used as root entropy to derive seed or secret
private key same as seed or secret for key pair
seed or secret or private key is crypto suite length dependent random bytes
public key
txn.put(
did.encode(),
json.dumps(certifiable_data).encode("utf-8")
)
raw_data = txn.get(did.encode())
if raw_data is None:
return None
return json.loads(raw_data)
ked = json.loads(raw[:size].decode("utf-8"))
raw = json.dumps(ked, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
"""
import math
import os
from collections import namedtuple, deque
from dataclasses import dataclass, asdict, field
import pysodium
from hio.base import doing
from .. import kering
from .. import core, help
from ..core import coring
from ..db import dbing, subing, koming
from ..help import helping
logger = help.ogler.getLogger()
Algoage = namedtuple("Algoage", 'randy salty group extern')
Algos = Algoage(randy='randy', salty='salty', group="group", extern="extern") # randy is rerandomize, salty is use salt
class ExternModule:
"""Base class for external key management.
Subclasses implement key generation and signing via external hardware
security devices. The interface mirrors Manager.incept(), Manager.rotate(),
and Manager.sign().
"""
def incept(self, icodes=None, icount=1, icode=coring.MtrDex.Ed25519_Seed,
ncodes=None, ncount=1, ncode=coring.MtrDex.Ed25519_Seed,
dcode=coring.MtrDex.Blake3_256,
transferable=True, temp=False, **kwa):
"""Create keys on external device.
Returns:
tuple: (verfers, digers) where verfers is list of Verfer and
digers is list of Diger for next keys
"""
raise NotImplementedError
def rotate(self, pre, ncodes=None, ncount=1,
ncode=coring.MtrDex.Ed25519_Seed,
dcode=coring.MtrDex.Blake3_256,
transferable=True, temp=False, **kwa):
"""Rotate keys on external device.
Returns:
tuple: (verfers, digers) where verfers is list of new current
Verfer and digers is list of Diger for next keys
"""
raise NotImplementedError
def sign(self, ser, pubs=None, verfers=None, indexed=True,
indices=None, ondices=None, **kwa):
"""Sign using external device.
Returns:
list: list of Siger instances if indexed else list of Cigar instances
"""
raise NotImplementedError
@dataclass()
class PubLot:
"""
Public key list with indexes and datetime created
Attributes:
pubs (list): list of fully qualified Base64 public keys. Defaults to empty.
ridx (int): rotation index of set of public keys at establishment event.
Includes of key set at inception event is 0.
kidx (int): key index of starting key in key set in sequence wrt to all
public keys. Example if each set has 3 keys then ridx 2 has
kidx of 2*3 = 6.
dt (str): datetime in ISO8601 format of when key set was first created
"""
pubs: list = field(default_factory=list) # list qb64 public keys.
ridx: int = 0 # index of rotation (est event) that uses public key set
kidx: int = 0 # index of key in sequence of public keys
dt: str = "" # datetime ISO8601 when key set created
def __iter__(self):
return iter(asdict(self))
@dataclass()
class PreSit:
"""
Prefix's public key situation (sets of public kets)
"""
old: PubLot = field(default_factory=PubLot) # previous publot
new: PubLot = field(default_factory=PubLot) # newly current publot
nxt: PubLot = field(default_factory=PubLot) # next public publot
def __iter__(self):
return iter(asdict(self))
@dataclass()
class PrePrm:
"""
Prefix's parameters for creating new key pairs
"""
pidx: int = 0 # prefix index for this keypair sequence
algo: str = Algos.salty # salty default uses indices and salt to create new key pairs
salt: str = '' # empty salt used for salty algo.
stem: str = '' # default unique path stem for salty algo
tier: str = '' # security tier for stretch index salty algo
def __iter__(self):
return iter(asdict(self))
@dataclass()
class PubSet:
"""
Prefix's public key set (list) at rotation index ridx
"""
pubs: list = field(default_factory=list) # list qb64 public keys.
def __iter__(self):
return iter(asdict(self))
def riKey(pre, ri):
"""
Returns bytes DB key from concatenation with '.' of qualified Base64 prefix
bytes pre and int ri (rotation index) of key rotation.
Inception has ri == 0
"""
if hasattr(pre, "encode"):
pre = pre.encode("utf-8") # convert str to bytes
return (b'%s.%032x' % (pre, ri))
def openKS(name="test", **kwa):
"""
Returns contextmanager generated by openLMDB but with Keeper instance as
KeyStore
default name="test"
default temp=True,
openLMDB Parameters:
cls is Class instance of subclass instance
name is str name of LMDBer dirPath so can have multiple databasers
at different directory path names thar each use different name
temp is Boolean, True means open in temporary directory, clear on close
Otherwise open in persistent directory, do not clear on close
"""
return dbing.openLMDB(cls=Keeper, name=name, **kwa)
# Env var for configuring LMDB size for the Keeper database
KERIKeeperMapSizeKey = "KERI_KEEPER_MAP_SIZE"
class Keeper(dbing.LMDBer):
"""
Keeper sets up named sub databases for key pair storage (KS).
Methods provide key pair creation, storage, and data signing.
Attributes: (inherited)
name (str): unique path component used in directory or file path name
base (str): another unique path component inserted before name
temp (bool): True means use /tmp directory
headDirPath is head directory path
path is full directory path
perm is numeric os permissions for directory and/or file(s)
filed (bool): True means .path ends in file.
False means .path ends in directory
mode (str): file open mode if filed
fext (str): file extension if filed
file (File)
opened is Boolean, True means directory created and if file then file
is opened. False otherwise
env (lmdb.env): LMDB main (super) database environment
readonly (bool): True means open LMDB env as readonly
Attributes:
gbls (subing.Suber): named sub DB whose values are global parameters
for all prefixes
Key is parameter labels
Value is parameter
parameters:
aeid (bytes): fully qualified qb64 non-transferable identifier
prefix for authentication via signing and asymmetric encryption
of secrets using the associated (public, private) key pair.
Secrets include both salts and private keys for all key sets
in keeper. Defaults to empty which means no authentication
or encryption of key sets.
pidx (bytes): hex index of next prefix key-pair sequence to be incepted
algo (str): default root algorithm for generating key pair
salt (bytes): root salt for generating key pairs
tier (bytes): default root security tier for root salt
pris (subing.CryptSignerSuber): named sub DB whose keys are public key
from key pair and values are private keys from key pair
Key is public key (fully qualified qb64)
Value is private key (fully qualified qb64)
pres (subing.CesrSuber): named sub DB whose values are prefixes or first
public keys
Key is first public key in key sequence for a prefix (fully qualified qb64)
Value is prefix or first public key (temporary) (fully qualified qb64)
prms (koming.Komer): named sub DB whose values are serialized dicts of
PrePrm instance
Key is identifier prefix (fully qualified qb64)
Value is serialized parameter dict of public key parameters
{
pidx: ,
algo: ,
salt: ,
stem: ,
tier: ,
}
sits (koming.Komer): named sub DB whose values are serialized dicts of
PreSit instance
Key is identifier prefix (fully qualified qb64)
Value is serialized parameter dict of public key situation
{
old: { pubs: ridx:, kidx, dt:},
new: { pubs: ridx:, kidx:, dt:},
nxt: { pubs: ridx:, kidx:, dt:}
}
.pubs (koming.Komer): named sub DB whose values are serialized lists of
public keys
Enables lookup of public keys from prefix and ridx for replay of
public keys by prefix in establishment event order.
Key is prefix.ridx (rotation index as 32 char hex string)
use riKey(pre, ri)
Value is serialized list of fully qualified public keys that are the
current signing keys after the rotation given by rotation index
Properties:
"""
TailDirPath = "keri/ks"
AltTailDirPath = ".keri/ks"
TempPrefix = "keri_ks_"
MaxNamedDBs = 10
def __init__(self, headDirPath=None, perm=None, reopen=False, **kwa):
"""
Setup named sub databases.
Inherited Parameters:
name is str directory path name differentiator for main database
When system employs more than one keri database, name allows
differentiating each instance by name
default name='main'
temp is boolean, assign to .temp
True then open in temporary directory, clear on close
Othewise then open persistent directory, do not clear on close
default temp=False
headDirPath is optional str head directory pathname for main database
If not provided use default .HeadDirpath
default headDirPath=None so uses self.HeadDirPath
perm is numeric optional os dir permissions mode
default perm=None so do not set mode
reopen is boolean, IF True then database will be reopened by this init
default reopen=True
Notes:
dupsort=True for sub DB means allow unique (key,pair) duplicates at a key.
Duplicate means that is more than one value at a key but not a redundant
copies a (key,value) pair per key. In other words the pair (key,value)
must be unique both key and value in combination.
Attempting to put the same (key,value) pair a second time does
not add another copy.
Duplicates are inserted in lexocographic order by value, insertion order.
"""
if perm is None:
perm = self.Perm # defaults to restricted permissions for non temp
mapSize = os.getenv(dbing.KERIKeeperMapSizeKey) or os.getenv(dbing.KERILMDBMapSizeKey)
if mapSize is not None:
try:
self.MapSize = int(mapSize)
except ValueError:
logger.error(f"LMDB map size environment variable must be an integer value > 1! "
f"Use {dbing.KERIKeeperMapSizeKey} or {dbing.KERILMDBMapSizeKey}")
raise
super(Keeper, self).__init__(headDirPath=headDirPath, perm=perm,
reopen=reopen, **kwa)
def reopen(self, **kwa):
"""
Open sub databases
"""
opened = super(Keeper, self).reopen(**kwa)
# Create by opening first time named sub DBs within main DB instance
# Names end with "." as sub DB name must include a non Base64 character
# to avoid namespace collisions with Base64 identifier prefixes.
self.gbls = subing.Suber(db=self, subkey='gbls.')
self.pris = subing.CryptSignerSuber(db=self, subkey='pris.')
self.prxs = subing.CesrSuber(db=self,
subkey='prxs.',
klas=core.Cipher)
self.nxts = subing.CesrSuber(db=self,
subkey='nxts.',
klas=core.Cipher)
self.smids = subing.CatCesrIoSetSuber(db=self,
subkey='smids.',
klas=(coring.Prefixer, coring.Seqner))
self.rmids = subing.CatCesrIoSetSuber(db=self,
subkey='rmids.',
klas=(coring.Prefixer, coring.Seqner))
self.pres = subing.CesrSuber(db=self,
subkey='pres.',
klas=coring.Prefixer)
self.prms = koming.Komer(db=self,
subkey='prms.',
schema=PrePrm,) # New Prefix Parameters
self.sits = koming.Komer(db=self,
subkey='sits.',
schema=PreSit,) # Prefix Situation
self.pubs = koming.Komer(db=self,
subkey='pubs.',
schema=PubSet,) # public key set at pre.ridx
return self.opened
class KeeperDoer(doing.Doer):
"""
Basic Keeper Doer ( LMDB Database )
Inherited Attributes:
.done is Boolean completion state:
True means completed
Otherwise incomplete. Incompletion maybe due to close or abort.
Attributes:
.keeper is Keeper or LMDBer subclass
Inherited Properties:
.tyme is float relative cycle time of associated Tymist .tyme obtained
via injected .tymth function wrapper closure.
.tymth is function wrapper closure returned by Tymist .tymeth() method.
When .tymth is called it returns associated Tymist .tyme.
.tymth provides injected dependency on Tymist tyme base.
.tock is float, desired time in seconds between runs or until next run,
non negative, zero means run asap
Properties:
Methods:
.wind injects ._tymth dependency from associated Tymist to get its .tyme
.__call__ makes instance callable
Appears as generator function that returns generator
.do is generator method that returns generator
.enter is enter context action method
.recur is recur context action method or generator method
.exit is exit context method
.close is close context method
.abort is abort context method
Hidden:
._tymth is injected function wrapper closure returned by .tymen() of
associated Tymist instance that returns Tymist .tyme. when called.
._tock is hidden attribute for .tock property
"""
def __init__(self, keeper, **kwa):
"""
Parameters:
keeper is Keeper instance
"""
super(KeeperDoer, self).__init__(**kwa)
self.keeper = keeper
def enter(self, *, temp=False):
""""""
if not self.keeper.opened:
self.keeper.reopen()
def exit(self):
""""""
self.keeper.close(clear=self.keeper.temp)
class Creator:
"""
Class for creating a key pair based on algorithm.
Attributes:
Properties:
Methods:
.create is method to create key pair
Hidden:
"""
def __init__(self, **kwa):
"""
Setup Creator.
Parameters:
"""
def create(self, **kwa):
"""
Returns tuple of signers one per key pair
"""
return []
@property
def salt(self):
"""
salt property getter
"""
return ''
@property
def stem(self):
"""
stem property getter
"""
return ''
@property
def tier(self):
"""
tier property getter
"""
return ''
class RandyCreator(Creator):
"""
Class for creating a key pair based on re-randomizing each seed algorithm.
Attributes:
Properties:
Methods:
.create is method to create key pair
Hidden:
"""
def __init__(self, **kwa):
"""
Setup Creator.
Parameters:
"""
super(RandyCreator, self).__init__(**kwa)
def create(self, codes=None, count=1, code=coring.MtrDex.Ed25519_Seed,
transferable=True, **kwa):
"""
Returns list of signers one per kidx in kidxs
Parameters:
codes is list of derivation codes one per key pair to create
count is count of key pairs to create is codes not provided
code is derivation code to use for count key pairs if codes not provided
transferable is Boolean, True means use trans deriv code. Otherwise nontrans
"""
signers = []
if not codes: # if not codes make list len count of same code
codes = [code for i in range(count)]
for code in codes:
signers.append(core.Signer(code=code, transferable=transferable))
return signers
class SaltyCreator(Creator):
"""
Class for creating a key pair based on random salt plus path stretch algorithm.
Attributes:
.salter is salter instance
Properties:
Methods:
.create is method to create key pair
Hidden:
._salter holds instance for .salter property
"""
def __init__(self, salt=None, stem=None, tier=None, **kwa):
"""
Setup Creator.
Parameters:
salt is unique salt from which to derive private key
stem is path modifier used with salt to derive private keys.
if stem is None then uses pidx
tier is derivation criticality that determines how much hashing to use.
"""
super(SaltyCreator, self).__init__(**kwa)
self.salter = core.Salter(qb64=salt, tier=tier)
self._stem = stem if stem is not None else ''
@property
def salt(self):
"""
salt property getter
"""
return self.salter.qb64
@property
def stem(self):
"""
stem property getter
"""
return self._stem
@property
def tier(self):
"""
tier property getter
"""
return self.salter.tier
def create(self, codes=None, count=1, code=coring.MtrDex.Ed25519_Seed,
pidx=0, ridx=0, kidx=0, transferable=True, temp=False, **kwa):
"""
Returns list of signers one per kidx in kidxs
Parameters:
codes is list of derivation codes one per key pair to create
count is count of key pairs to create is codes not provided
code is derivation code to use for count key pairs if codes not provided
pidx is int prefix index for key pair sequence
ridx is int rotation index for key pair set
kidx is int starting key index for key pair set
transferable is Boolean, True means use trans deriv code. Otherwise nontrans
temp is Boolean True means use temp stretch otherwise use time set
by tier for streching
"""
signers = []
if not codes: # if not codes make list len count of same code
codes = [code for i in range(count)]
stem = self.stem if self.stem else "{:x}".format(pidx) # if not stem use pidx
for i, code in enumerate(codes):
path = "{}{:x}{:x}".format(stem, ridx, kidx + i)
signers.append(self.salter.signer(path=path,
code=code,
transferable=transferable,
tier=self.tier,
temp=temp))
return signers
class Creatory:
"""
Factory class for creating Creator subclasses to create key pairs based on
the provided algorithm.
Usage: creator = Creatory(algo='salty').make(salt=b'0123456789abcdef')
Attributes:
Properties:
Methods:
.create is method to create key pair
Hidden:
._create is method reference set to one of algorithm methods
._novelCreate
._indexCreate
"""
def __init__(self, algo=Algos.salty):
"""
Setup Creator.
Parameters:
algo is str code for algorithm
"""
if algo == Algos.randy:
self._make = self._makeRandy
elif algo == Algos.salty:
self._make = self._makeSalty
else:
raise ValueError("Unsupported creation algorithm ={}.".format(algo))
def make(self, **kwa):
"""
Returns Creator subclass based on inited algo
"""
return (self._make(**kwa))
def _makeRandy(self, **kwa):
"""
"""
return RandyCreator(**kwa)
def _makeSalty(self, **kwa):
"""
"""
return SaltyCreator(**kwa)
# default values to init manager's globals database
Initage = namedtuple("Initage", 'aeid pidx salt tier')
class Manager:
"""Manages key pairs creation, storage, and signing
Class for managing key pair creation, storage, retrieval, and message signing.
Attributes:
ks (Keeper): key store LMDB database instance for storing public and private keys
encrypter (core.Encrypter): instance for encrypting secrets. Public
encryption key is derived from aeid (public signing key)
decrypter (core.Decrypter): instance for decrypting secrets. Private
decryption key is derived seed (private signing key seed)
inited (bool): True means fully initialized wrt database.
False means not yet fully initialized
Attributes (Hidden):
_seed (str): qb64 private-signing key (seed) for the aeid from which
the private decryption key is derived. If aeid stored in
database is not empty then seed may required to do any key
management operations. The seed value is memory only and MUST NOT
be persisted to the database for the manager with which it is used.
It MUST only be loaded once when the process that runs the Manager
is initialized. Its presence acts as an authentication, authorization,
and decryption secret for the Manager and must be stored on
another device from the device that runs the Manager.
Properties:
aeid (str): authentication and encryption fully qualified qb64
non-transferable identifier prefix for authentication via signing
and asymmetric encryption of secrets using the associated
(public, private) key pair. Secrets include both salts and private
keys for all key sets in keeper. Defaults to empty which means no
authentication or encryption of key sets. Use initial attribute because
keeper may not be open on init.
pidx (int): pidx prefix index.
Use initial attribute because keeper
may not be open on init. Each sequence gets own pidx. Enables
unique recreatable salting of key sequence based on pidx.
salt (str): qb64 of root salt. Makes random root salt if not provided
initial salt. Use inital attribute because keeper may not be
open on init.
tier (str): initial security tier as value of Tierage. Use initial attribute
because keeper may not be open on init
Methods:
"""
def __init__(self, *, ks=None, seed=None, extern=None, **kwa):
"""
Setup Manager.
Parameters:
ks (Keeper): key store instance (LMDB)
seed (str): qb64 private-signing key (seed) for the aeid from which
the private decryption key may be derived. If aeid stored in
database is not empty then seed may required to do any key
management operations. The seed value is memory only and MUST NOT
be persisted to the database for the manager with which it is used.
It MUST only be loaded once when the process that runs the Manager
is initialized. Its presence acts as an authentication, authorization,
and decryption secret for the Manager and must be stored on
another device from the device that runs the Manager.
Currently only code MtrDex.Ed25519_Seed is supported.
extern (ExternModule): optional external key management module external
device integration. When provided, enables Algos.extern for incept,
rotate, and sign.
Parameters: Passthrough to .setup for later initialization
aeid (str): qb64 of non-transferable identifier prefix for
authentication and encryption of secrets in keeper. If provided
aeid (not None) and different from aeid stored in database then
all secrets are re-encrypted using new aeid. In this case the
provided seed must not be empty. A change in aeid should require
a second authentication mechanism besides the seed.
pidx (int): index of next new created key pair sequence bound to a
given identifier prefix. Each sequence gets own pidx. Enables
unique recreatable salting of key sequence based on pidx.
salt (str): qb64 of root salt. Makes random root salt if not provided
tier (str): default security tier (Tierage) for root salt
"""
self.ks = ks if ks is not None else Keeper(reopen=True)
self.encrypter = None
self.decrypter = None
self._seed = seed if seed is not None else ""
self.extern = extern
self.inited = False
# save keyword arg parameters to init later if db not opened yet
self._inits = kwa
if self.ks.opened: # allows keeper db to opened asynchronously
self.setup(**self._inits) # first call to .setup with initialize database
def setup(self, aeid=None, pidx=None, algo=None, salt=None, tier=None):
"""
Setups manager root or global attributes and properties
Assumes that .keeper db is open.
If .keeper.gbls sub database has not been initialized for the first time
then initializes from ._inits. This allows dependency injection of
keepr db into manager instance prior to keeper db being opened to
accomodate asynchronous process setup of db resources. Putting the db
initialization here enables asynchronous opening of keeper db after
keeper instance is instantiated. First call to .setup will initialize
keeper db defaults if never before initialized (vacuous initialization).
Parameters:
aeid (str): qb64 of non-transferable identifier prefix for
authentication and encryption of secrets in keeper. If provided
aeid (not None) and different from aeid stored in database then
all secrets are re-encrypted using new aeid. In this case the
provided seed must not be empty. A change in aeid should require
a second authentication mechanism besides the secret.
aeid same as current aeid no change innocuous
aeid different but empty which unencrypts and removes aeid
aeid different not empty which reencrypts and updates aeid
pidx (int): index of next new created key pair sequence for given
identifier prefix
algo (str): root algorithm (randy or salty) for creating key pairs
salt (str): qb64 of root salt. Makes random root salt if not provided
tier (str): default security tier (Tierage) for root salt
"""
if not self.ks.opened:
raise kering.ClosedError("Attempt to setup Manager closed keystore"
" database .ks.")
if aeid is None:
aeid = ''
if pidx is None:
pidx = 0
if algo is None:
algo = Algos.salty
if salt is None:
salt = core.Salter().qb64
else:
if core.Salter(qb64=salt).qb64 != salt:
raise ValueError(f"Invalid qb64 for salt={salt}.")
if tier is None:
tier = core.Tiers.low
# update database if never before initialized
if self.pidx is None: # never before initialized
self.pidx = pidx # init to default
if self.algo is None: # never before initialized
self.algo = algo
if self.salt is None: # never before initialized
self.salt = salt
if self.tier is None: # never before initialized
self.tier = tier # init to default
# must do this after salt is initialized so gets re-encrypted correctly
if not self.aeid: # never before initialized
self.updateAeid(aeid, self.seed)
else:
self.encrypter = core.Encrypter(verkey=self.aeid) # derive encrypter from aeid
if not self.seed or not self.encrypter.verifySeed(self.seed):
raise kering.AuthError("Last seed missing or provided last seed "
"not associated with last aeid={}."
"".format(self.aeid))
self.decrypter = core.Decrypter(seed=self.seed)
self.inited = True
def updateAeid(self, aeid, seed):
"""
Given seed belongs to aeid and encrypter, update aeid and re-encrypt all
secrets
Parameters:
aeid (Optional(str)): qb64 of new auth encrypt id (public signing key)
aeid may match current aeid no change innocuous
aeid may be empty which unencrypts and removes aeid
aeid may be different not empty which reencrypts
seed (str): qb64 of new seed from which new aeid is derived (private signing
key seed)
"""
if self.aeid: # check that last current seed matches last current .aeid
# verifies seed belongs to aeid
if not self.seed or not self.encrypter.verifySeed(self.seed):
raise kering.AuthError("Last seed missing or provided last seed "
"not associated with last aeid={}."
"".format(self.aeid))
if aeid: # aeid provided
if aeid != self.aeid: # changing to a new aeid so update .encrypter
self.encrypter = core.Encrypter(verkey=aeid) # derive encrypter from aeid
# verifies new seed belongs to new aeid
if not seed or not self.encrypter.verifySeed(seed):
raise kering.AuthError("Seed missing or provided seed not associated"
" with provided aeid={}.".format(aeid))
else: # changing to empty aeid so new encrypter is None
self.encrypter = None
# fetch all secrets from db, decrypt all secrets with self.decrypter
# unless they decrypt automatically on fetch and then re-encrypt with
# encrypter update db with re-encrypted values
# re-encypt root salt secret, .salt property is automatically decrypted on fetch
if (salt := self.salt) is not None: # decrypted salt
self.salt = salt
# self.salt = self.encrypter.encrypt(ser=salt).qb64 if self.encrypter else salt
# other secrets
if self.decrypter:
# re-encrypt root salt secrets by prefix parameters .prms
for keys, data in self.ks.prms.getItemIter(): # keys is tuple of pre qb64
if data.salt:
salter = self.decrypter.decrypt(qb64=data.salt)
data.salt = (self.encrypter.encrypt(prim=salter).qb64
if self.encrypter else salter.qb64)
self.ks.prms.pin(keys, val=data)
# private signing key seeds
# keys is tuple == (verkey.qb64,) .pris database auto decrypts
for keys, signer in self.ks.pris.getItemIter(decrypter=self.decrypter):
self.ks.pris.pin(keys, signer, encrypter=self.encrypter)
self.ks.gbls.pin("aeid", aeid) # set aeid in db
self._seed = seed # set .seed in memory
# update .decrypter
self.decrypter = core.Decrypter(seed=seed) if seed else None
@property
def seed(self):
"""
seed property getter from ._seed.
seed (str): qb64 from which aeid is derived
"""
return self._seed
@property
def aeid(self):
"""
aeid property getter from key store db.
Assumes db initialized.
aeid is qb64 auth encrypt id prefix
"""
return self.ks.gbls.get('aeid')
@property
def pidx(self):
"""
pidx property getter from key store db.
Assumes db initialized.
pidx is prefix index int for next new key sequence
"""
if (pidx := self.ks.gbls.get("pidx")) is not None:
return int(pidx, 16)
return pidx # None
@pidx.setter
def pidx(self, pidx):
"""
pidx property setter to key store db.
pidx is prefix index int for next new key sequence
"""
self.ks.gbls.pin("pidx", "%x" % pidx)
@property
def algo(self):
"""
also property getter from key store db.
Assumes db initialized.
algo is default root algorithm for creating key pairs
"""
return self.ks.gbls.get('algo')
@algo.setter
def algo(self, algo):
"""
algo property setter to key store db.
algo is default root algorithm for creating key pairs
"""
self.ks.gbls.pin('algo', algo)
@property
def salt(self):
"""
salt property getter from key store db.
Assumes db initialized.
salt is default root salt for new key sequence creation
"""
salt = self.ks.gbls.get('salt')
if self.decrypter: # given .decrypt secret salt must be encrypted in db
return self.decrypter.decrypt(qb64=salt).qb64
return salt
@salt.setter
def salt(self, salt):
"""
salt property setter to key store db.
Parameters:
salt (str): qb64 default root salt for new key sequence creation
may be plain text or cipher text handled by updateAeid
"""
if self.encrypter:
salt = self.encrypter.encrypt(ser=salt, code=core.MtrDex.X25519_Cipher_Salt).qb64
self.ks.gbls.pin('salt', salt)
@property
def tier(self):
"""
tier property getter from key store db.
Assumes db initialized.
tier is default root security tier for new key sequence creation
"""
return self.ks.gbls.get('tier')
@tier.setter
def tier(self, tier):
"""
tier property setter to key store db.
tier is default root security tier for new key sequence creation
"""
self.ks.gbls.pin('tier', tier)
def incept(self, icodes=None, icount=1, icode=coring.MtrDex.Ed25519_Seed,
ncodes=None, ncount=1, ncode=coring.MtrDex.Ed25519_Seed,
dcode=coring.MtrDex.Blake3_256,
algo=None, salt=None, stem=None, tier=None, rooted=True,
transferable=True, temp=False):
"""
Returns tuple (verfers, digers) for inception event where
verfers is list of current public key verfers
public key is verfer.qb64
digers is list of next public key digers
digest to xor is diger.raw