11from __future__ import annotations
22
3+ from functools import cached_property
4+ from typing import TYPE_CHECKING
5+ from uuid import UUID
6+
7+ from dissect .util .ts import wintimestamp
8+
9+ from dissect .database .ese .ntds .c_ds import c_ds
310from dissect .database .ese .ntds .objects .leaf import Leaf
411
12+ if TYPE_CHECKING :
13+ from datetime import datetime
14+
515
616class Secret (Leaf ):
717 """Represents a secret object in the Active Directory.
@@ -11,3 +21,79 @@ class Secret(Leaf):
1121 """
1222
1323 __object_class__ = "secret"
24+
25+ def __repr_body__ (self ) -> str :
26+ return f"name={ self .name !r} last_set_time={ self .last_set_time } prior_set_time={ self .prior_set_time } "
27+
28+ @property
29+ def current_value (self ) -> bytes :
30+ """Return the current value of the secret."""
31+ return self .get ("currentValue" )
32+
33+ @property
34+ def last_set_time (self ) -> datetime | None :
35+ """Return the last set time of the secret."""
36+ if (ts := self .get ("lastSetTime" )) is not None :
37+ return wintimestamp (ts )
38+ return None
39+
40+ @property
41+ def prior_value (self ) -> bytes :
42+ """Return the prior value of the secret."""
43+ return self .get ("priorValue" )
44+
45+ @property
46+ def prior_set_time (self ) -> datetime | None :
47+ """Return the prior set time of the secret."""
48+ if (ts := self .get ("priorSetTime" )) is not None :
49+ return wintimestamp (ts )
50+ return None
51+
52+
53+ class BackupKey :
54+ """Represents a DPAPI backup key object in the Active Directory."""
55+
56+ def __init__ (self , secret : Secret ):
57+ self .secret = secret
58+
59+ def __repr__ (self ) -> str :
60+ return f"<BackupKey guid={ self .guid } version={ self .version } >"
61+
62+ @cached_property
63+ def guid (self ) -> UUID :
64+ """The GUID of the backup key."""
65+ return UUID (self .secret .name .removeprefix ("BCKUPKEY_" ).removesuffix (" Secret" ))
66+
67+ @cached_property
68+ def version (self ) -> int :
69+ """The version of the backup key."""
70+ return c_ds .DWORD (self .secret .current_value )
71+
72+ @cached_property
73+ def is_legacy (self ) -> bool :
74+ """Whether the backup key is a legacy key (version 1)."""
75+ return self .version == 1
76+
77+ @cached_property
78+ def key (self ) -> bytes :
79+ """The key bytes of the backup key, for legacy keys (version 1)."""
80+ if self .version == 1 :
81+ return self .secret .current_value [4 :]
82+ raise TypeError (f"Backup key version { self .version } does not have a single key value" )
83+
84+ @cached_property
85+ def private_key (self ) -> bytes :
86+ """The private key bytes of the backup key, for version 2 keys."""
87+ if self .version == 2 :
88+ private_length = c_ds .DWORD (self .secret .current_value [4 :8 ])
89+ return self .secret .current_value [12 : 12 + private_length ]
90+ raise TypeError (f"Backup key version { self .version } does not have a private key value" )
91+
92+ @cached_property
93+ def public_key (self ) -> bytes :
94+ """The public key bytes of the backup key, for version 2 keys."""
95+ if self .version == 2 :
96+ private_length = c_ds .DWORD (self .secret .current_value [4 :8 ])
97+ public_length = c_ds .DWORD (self .secret .current_value [8 :12 ])
98+ return self .secret .current_value [12 + private_length : 12 + private_length + public_length ]
99+ raise TypeError (f"Backup key version { self .version } does not have a public key value" )
0 commit comments