Skip to content

Commit c9f4ea7

Browse files
committed
option to display Bitlocker keys
1 parent 1ae51ae commit c9f4ea7

File tree

4 files changed

+124
-101
lines changed

4 files changed

+124
-101
lines changed

laps-client/README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# LAPS4LINUX Client
2-
The management client enables administrators to view the current (decrypted) local admin passwords. It can be used from command line or as graphical application.
2+
The management client enables administrators to easily view the current (decrypted) local admin passwords and the Bitlocker recovery key too. It can be used from command line or as graphical application.
33

44
### Graphical User Interface (GUI)
55
![screenshot](../.github/screenshot.png)
@@ -61,7 +61,10 @@ You can create a preset config file `/etc/laps-client.json` which will be loaded
6161
- `use-starttls`: Boolean which indicates wheter to use StartTLS on unencrypted LDAP connections (requires valid server certificate).
6262
- `username`: The username for LDAP simple binds. For Microsoft AD, you need to append the domain (`user@example.com`). For OpenLDAP, you need to enter your user DN (`dn=user,dc=example,dc=com`).
6363
- `use-kerberos`: Boolean which indicates wheter to use Kerberos for LDAP bind before falling back to simple bind.
64-
- `ldap-attributes`: A dict of LDAP attributes to display. Dict key is the display name and the corresponding value is the LDAP attribute name. The dict value can also be a list of strings. Then, the first non-empty LDAP attribute will be displayed.
64+
- `ldap-attributes`: A dict of LDAP attributes to display.
65+
- Dict key is the display name and the corresponding value is the LDAP attribute name.
66+
- The dict value can also be a list of strings. Then, the first non-empty LDAP attribute will be displayed. This is useful when migrating to Native LAPS - you can display the new attribute value if exists, otherwise the old attribute value of Legacy LAPS is shown.
67+
- When appending `sub:` to the dict value (= LDAP attribute name), the sub-enrties of the computer object are searched. This is useful for querying the Bitlocker recovery key (`sub:msFVE-RecoveryPassword`). Make sure that you have permission to view the Bitlocker keys!
6568
- `ldap-attribute-password`: The LDAP attribute name which contains the admin password. The client will try to decrypt this value (in case of Native LAPS) and use it for Remmina connections. Can also be a list of strings.
6669
- `ldap-attribute-password-expiry`: The LDAP attribute name which contains the admin password expiration date. The client will write the updated expiration date into this attribute. Can also be a list of strings.
6770
- `ldap-attribute-password-history`: The LDAP attribute name which contains the admin password history. The client will try to decrypt this value (in case of Native LAPS) and use it to display the password history. Can also be a list of strings.

laps-client/laps-client-settings.json.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"ldap-attributes": {
3737
"Operating System": "operatingSystem",
3838
"Last Logon Timestamp": "lastLogonTimestamp",
39+
"Bitlocker Recovery Key": "sub:msFVE-RecoveryPassword",
3940
"Administrator Password": [
4041
"msLAPS-EncryptedPassword",
4142
"msLAPS-Password",

laps-client/laps_client/laps_cli.py

Lines changed: 53 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -157,57 +157,66 @@ def queryAttributes(self):
157157
)
158158
# display result
159159
for entry in self.connection.entries:
160-
# evaluate attributes of interest
161-
for title, attribute in self.GetAttributesAsDict().items():
162-
value = None
163-
if(isinstance(attribute, list)):
164-
for _attribute in attribute:
165-
# use first non-empty attribute
166-
if(str(_attribute) in entry and entry[str(_attribute)]):
167-
value = entry[str(_attribute)]
168-
attribute = str(_attribute)
169-
break
170-
elif(str(attribute) in entry):
171-
value = entry[str(attribute)]
172-
173-
# handle non-existing attributes
174-
if(value == None):
175-
self.pushResult(str(title), '')
176-
177-
# if this is the password attribute -> try to parse Native LAPS format
178-
elif(len(value) > 0 and
179-
(str(attribute) == self.cfgLdapAttributePassword or (isinstance(self.cfgLdapAttributePassword, list) and str(attribute) in self.cfgLdapAttributePassword))
180-
):
181-
password, username, timestamp = self.parseLapsValue(value.values[0])
182-
if(not username or not password):
183-
self.pushResult(str(title), password)
184-
else:
185-
self.pushResult(str(title), password+' ('+username+') ('+timestamp+')')
186-
187-
# if this is the encrypted password history attribute -> try to parse Native LAPS format
188-
elif(len(value) > 0 and
189-
(str(attribute) == self.cfgLdapAttributePasswordHistory or (isinstance(self.cfgLdapAttributePasswordHistory, list) and str(attribute) in self.cfgLdapAttributePasswordHistory))
190-
):
191-
for _value in value.values:
192-
password, username, timestamp = self.parseLapsValue(_value)
160+
# we are looking at the main computer object
161+
if(entry.entry_dn == self.tmpDn):
162+
# evaluate attributes of interest
163+
for title, attribute in self.GetAttributesAsDict().items():
164+
if(attribute[:4] == 'sub:'): continue
165+
value = None
166+
if(isinstance(attribute, list)):
167+
for _attribute in attribute:
168+
# use first non-empty attribute
169+
if(str(_attribute) in entry and entry[str(_attribute)]):
170+
value = entry[str(_attribute)]
171+
attribute = str(_attribute)
172+
break
173+
elif(str(attribute) in entry):
174+
value = entry[str(attribute)]
175+
176+
# handle non-existing attributes
177+
if(value == None):
178+
self.pushResult(str(title), '')
179+
180+
# if this is the password attribute -> try to parse Native LAPS format
181+
elif(len(value) > 0 and
182+
(str(attribute) == self.cfgLdapAttributePassword or (isinstance(self.cfgLdapAttributePassword, list) and str(attribute) in self.cfgLdapAttributePassword))
183+
):
184+
password, username, timestamp = self.parseLapsValue(value.values[0])
193185
if(not username or not password):
194186
self.pushResult(str(title), password)
195187
else:
196188
self.pushResult(str(title), password+' ('+username+') ('+timestamp+')')
197189

198-
# if this is the expiry date attribute -> format date
199-
elif(str(attribute) == self.cfgLdapAttributePasswordExpiry or (isinstance(self.cfgLdapAttributePasswordExpiry, list) and str(attribute) in self.cfgLdapAttributePasswordExpiry)):
200-
try:
201-
self.pushResult(str(title), str(value)+' ('+str(filetime_to_dt( int(str(value)) ))+')')
202-
except Exception as e:
203-
eprint('Error:', str(e))
190+
# if this is the encrypted password history attribute -> try to parse Native LAPS format
191+
elif(len(value) > 0 and
192+
(str(attribute) == self.cfgLdapAttributePasswordHistory or (isinstance(self.cfgLdapAttributePasswordHistory, list) and str(attribute) in self.cfgLdapAttributePasswordHistory))
193+
):
194+
for _value in value.values:
195+
password, username, timestamp = self.parseLapsValue(_value)
196+
if(not username or not password):
197+
self.pushResult(str(title), password)
198+
else:
199+
self.pushResult(str(title), password+' ('+username+') ('+timestamp+')')
200+
201+
# if this is the expiry date attribute -> format date
202+
elif(str(attribute) == self.cfgLdapAttributePasswordExpiry or (isinstance(self.cfgLdapAttributePasswordExpiry, list) and str(attribute) in self.cfgLdapAttributePasswordExpiry)):
203+
try:
204+
self.pushResult(str(title), str(value)+' ('+str(filetime_to_dt( int(str(value)) ))+')')
205+
except Exception as e:
206+
eprint('Error:', str(e))
207+
self.pushResult(str(title), str(value))
208+
209+
# display raw value
210+
else:
204211
self.pushResult(str(title), str(value))
205212

206-
# display raw value
207-
else:
208-
self.pushResult(str(title), str(value))
209-
210-
return
213+
# we are looking at a sub-item of the computer object, e.g. a BitLocker recovery key
214+
else:
215+
for title, attribute in self.GetAttributesAsDict().items():
216+
if(attribute[:4] != 'sub:'): continue
217+
subattribute = str(attribute[4:])
218+
if(subattribute in entry):
219+
self.pushResult(str(title), str(entry[subattribute]))
211220

212221
dpapiCache = dpapi_ng.KeyCache()
213222
def decryptPassword(self, blob):

laps-client/laps_client/laps_gui.py

Lines changed: 65 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -626,63 +626,73 @@ def queryAttributes(self):
626626
)
627627
# display result
628628
for entry in self.connection.entries:
629-
self.btnSetExpirationTime.setEnabled(True)
630-
self.btnSearchComputer.setEnabled(True)
631-
632-
# evaluate attributes of interest
633-
for title, attribute in self.GetAttributesAsDict().items():
634-
textBox = self.refLdapAttributesTextBoxes[str(title)]
635-
value = None
636-
if(isinstance(attribute, list)):
637-
for _attribute in attribute:
638-
# use first non-empty attribute
639-
if(str(_attribute) in entry and entry[str(_attribute)]):
640-
value = entry[str(_attribute)]
641-
attribute = str(_attribute)
642-
break
643-
elif(str(attribute) in entry):
644-
value = entry[str(attribute)]
645-
646-
# handle non-existing attributes
647-
if(value == None):
648-
self.updateTextboxText(textBox, '')
649-
650-
# if this is the password attribute -> try to parse Native LAPS format
651-
elif(len(value) > 0 and
652-
(str(attribute) == self.cfgLdapAttributePassword or (isinstance(self.cfgLdapAttributePassword, list) and str(attribute) in self.cfgLdapAttributePassword))
653-
):
654-
password, username, timestamp = self.parseLapsValue(value.values[0])
655-
self.updateTextboxText(textBox, str(password))
656-
if(username and password):
657-
self.cfgConnectUsername = username
658-
textBox.setToolTip(username+', '+timestamp)
659-
660-
# if this is the encrypted password history attribute -> try to parse Native LAPS format
661-
elif(len(value) > 0 and
662-
(str(attribute) == self.cfgLdapAttributePasswordHistory or (isinstance(self.cfgLdapAttributePasswordHistory, list) and str(attribute) in self.cfgLdapAttributePasswordHistory))
663-
):
664-
lines = []
665-
for _value in value.values:
666-
password, username, timestamp = self.parseLapsValue(_value)
667-
if(not username or not password):
668-
lines.append(str(password))
669-
else:
670-
lines.append(password+' '+username+' '+timestamp)
671-
self.updateTextboxText(textBox, "\n".join(lines))
672-
673-
# if this is the expiry date attribute -> format date
674-
elif(str(attribute) == self.cfgLdapAttributePasswordExpiry or (isinstance(self.cfgLdapAttributePasswordExpiry, list) and str(attribute) in self.cfgLdapAttributePasswordExpiry)):
675-
try:
676-
self.updateTextboxText(textBox, str(filetime_to_dt( int(str(value)) )) )
677-
except Exception as e:
678-
print(str(e))
629+
# we are looking at the main computer object
630+
if(entry.entry_dn == self.tmpDn):
631+
self.btnSetExpirationTime.setEnabled(True)
632+
self.btnSearchComputer.setEnabled(True)
633+
634+
# evaluate attributes of interest
635+
for title, attribute in self.GetAttributesAsDict().items():
636+
if(attribute[:4] == 'sub:'): continue
637+
textBox = self.refLdapAttributesTextBoxes[str(title)]
638+
value = None
639+
if(isinstance(attribute, list)):
640+
for _attribute in attribute:
641+
# use first non-empty attribute
642+
if(str(_attribute) in entry and entry[str(_attribute)]):
643+
value = entry[str(_attribute)]
644+
attribute = str(_attribute)
645+
break
646+
elif(str(attribute) in entry):
647+
value = entry[str(attribute)]
648+
649+
# handle non-existing attributes
650+
if(value == None):
651+
self.updateTextboxText(textBox, '')
652+
653+
# if this is the password attribute -> try to parse Native LAPS format
654+
elif(len(value) > 0 and
655+
(str(attribute) == self.cfgLdapAttributePassword or (isinstance(self.cfgLdapAttributePassword, list) and str(attribute) in self.cfgLdapAttributePassword))
656+
):
657+
password, username, timestamp = self.parseLapsValue(value.values[0])
658+
self.updateTextboxText(textBox, str(password))
659+
if(username and password):
660+
self.cfgConnectUsername = username
661+
textBox.setToolTip(username+', '+timestamp)
662+
663+
# if this is the encrypted password history attribute -> try to parse Native LAPS format
664+
elif(len(value) > 0 and
665+
(str(attribute) == self.cfgLdapAttributePasswordHistory or (isinstance(self.cfgLdapAttributePasswordHistory, list) and str(attribute) in self.cfgLdapAttributePasswordHistory))
666+
):
667+
lines = []
668+
for _value in value.values:
669+
password, username, timestamp = self.parseLapsValue(_value)
670+
if(not username or not password):
671+
lines.append(str(password))
672+
else:
673+
lines.append(password+' '+username+' '+timestamp)
674+
self.updateTextboxText(textBox, "\n".join(lines))
675+
676+
# if this is the expiry date attribute -> format date
677+
elif(str(attribute) == self.cfgLdapAttributePasswordExpiry or (isinstance(self.cfgLdapAttributePasswordExpiry, list) and str(attribute) in self.cfgLdapAttributePasswordExpiry)):
678+
try:
679+
self.updateTextboxText(textBox, str(filetime_to_dt( int(str(value)) )) )
680+
except Exception as e:
681+
print(str(e))
682+
self.updateTextboxText(textBox, str(value))
683+
684+
# display raw value
685+
else:
679686
self.updateTextboxText(textBox, str(value))
680687

681-
# display raw value
682-
else:
683-
self.updateTextboxText(textBox, str(value))
684-
685-
return
688+
# we are looking at a sub-item of the computer object, e.g. a BitLocker recovery key
689+
else:
690+
for title, attribute in self.GetAttributesAsDict().items():
691+
textBox = self.refLdapAttributesTextBoxes[str(title)]
692+
if(attribute[:4] != 'sub:'): continue
693+
subattribute = str(attribute[4:])
694+
if(subattribute in entry):
695+
self.updateTextboxText(textBox, str(entry[subattribute]))
686696

687697
def updateTextboxText(self, textBox, text):
688698
if(isinstance(textBox, QPlainTextEdit)):

0 commit comments

Comments
 (0)