def rotate_password(params, record_uid): """ Rotate the password for the specified record UID """ record_uid = record_uid.strip() if not record_uid: print('No record UID provided') return False if not params.record_cache: print('No record cache. Sync down first.') return False if not record_uid in params.record_cache: print('Record UID not found.') return False # get the record object cached_rec = params.record_cache[record_uid] # extract data and extra from record if 'data' in cached_rec: data = json.loads(cached_rec['data'].decode('utf-8')) else: data = {} if 'extra' in cached_rec: extra = json.loads(cached_rec['extra'].decode('utf-8')) else: extra = {} # check for edit permissions can_edit = False if 'can_edit' in cached_rec: if params.debug: print('Edit permissions found in record') can_edit = True # If record permission not there, check shared folders found_shared_folder_uid = '' if can_edit == False: for shared_folder_uid in params.shared_folder_cache: shared_folder = params.shared_folder_cache[shared_folder_uid] sf_key = shared_folder['shared_folder_key'] if 'records' in shared_folder: sf_records = shared_folder['records'] for sf_record in sf_records: if 'record_uid' in sf_record: if sf_record['record_uid'] == record_uid: found_shared_folder_uid = shared_folder_uid if 'can_edit' in sf_record: can_edit = True if params.debug: print('Edit permissions found in folder') break if not can_edit: print('You do not have permissions to edit this record.') return False if not params.server: raise CommunicationError('No server provided') if not params.user: raise CommunicationError('No username provided') # save previous password if params.debug: print('Data: ' + str(data)) if params.debug: print('Extra: ' + str(extra)) # generate friendly datestamp modified_time = int(round(time.time())) modified_time_milli = modified_time * 1000 datestamp = datetime.datetime.fromtimestamp( modified_time).strftime('%Y-%m-%d %H:%M:%S') # Backup old password in a custom field custom_dict = {} custom_dict['name'] = 'cmdr:Rotation @ '+str(datestamp) custom_dict['value'] = data['secret2'] custom_dict['type'] = 'text' # serialize this dict serialized = json.dumps(custom_dict) # load as json custom_dict_json = json.loads(serialized) # Append to the current structure data['custom'].append(custom_dict_json) if params.debug: print('Old password: '******'secret2'])) # load the data into a record object for convenience record_object = Record() record_object.load(data) new_password = generator.generate() # generate a new password with any specified rules rules = record_object.get("cmdr:rules") if rules: new_password = generator.generateFromRules(rules) # execute rotation plugin associated with this record plugin_name = record_object.get("cmdr:plugin") if plugin_name: print("Rotating with plugin " + str(plugin_name)) plugin = plugin_manager.get_plugin(plugin_name) if plugin: success = plugin.rotate(record_object, new_password) if success: if params.debug: print('Password rotation on target system is successful.') else: print('Password rotation failed') return False else: return False # Proceed with Keeper password rotation data['secret2'] = new_password if params.debug: print('New password: '******'secret2'])) if params.debug: print('New record data: ' + str(data)) # Update the record cache with the cleartext data if params.debug: print('data is ' + str(isinstance(data, dict))) if params.debug: print('params.record_cache is ' + \ str(isinstance(params.record_cache, dict))) # convert dict back to json then encode it params.record_cache[record_uid]['data'] = json.dumps(data).encode() if params.debug: print('New record: ' + str(params.record_cache[record_uid])) print('Data: ' + str(data)) print('Extra: ' + str(extra)) # Convert the data and extra dictionary to string object # with double quotes instead of single quotes data_serialized = json.dumps(data) extra_serialized = json.dumps(extra) if params.debug: print('data_serialized: ' + str(data_serialized)) if params.debug: print('extra_serialized: ' + str(extra_serialized)) # encrypt data and extra if not 'record_key_unencrypted' in params.record_cache[record_uid]: if plugin_name: print('Plugin updated password to: ' + new_password) raise CryptoError('No record_key_unencrypted found for ' + record_uid) if not 'record_key' in params.record_cache[record_uid]: if plugin_name: print('Plugin updated password to: ' + new_password) raise CryptoError('No record_key found for ' + record_uid) record_key_unencrypted = \ params.record_cache[record_uid]['record_key_unencrypted'] iv = os.urandom(16) cipher = AES.new(record_key_unencrypted, AES.MODE_CBC, iv) encrypted_data = iv + cipher.encrypt(pad(data_serialized)) iv = os.urandom(16) cipher = AES.new(record_key_unencrypted, AES.MODE_CBC, iv) encrypted_extra = iv + cipher.encrypt(pad(extra_serialized)) if params.debug: print('encrypted_data: ' + str(encrypted_data)) if params.debug: print('encrypted_extra: ' + str(encrypted_extra)) # note: decode() converts bytestream (b') to string encoded_data = base64.urlsafe_b64encode(encrypted_data).decode() encoded_extra = base64.urlsafe_b64encode(encrypted_extra).decode() if params.debug: print('encoded_data: ' + str(encoded_data)) if params.debug: print('encoded_extra: ' + str(encoded_extra)) # build a record object new_record = {} new_record['record_uid'] = record_uid new_record['version'] = 2 new_record['data'] = encoded_data new_record['extra'] = encoded_extra new_record['client_modified_time'] = modified_time_milli new_record['revision'] = params.record_cache[record_uid]['revision'] if found_shared_folder_uid: new_record['shared_folder_uid'] = found_shared_folder_uid if 'udata' in params.record_cache[record_uid]: new_record['udata'] = params.record_cache[record_uid]['udata'] if params.debug: print('new_record: ' + str(new_record)) # create updated records update_records = [] update_records.append(new_record) def make_json(params, update_records): return { 'client_time':current_milli_time(), 'device_id':'Commander', 'device_name':'Commander', 'command':'record_update', 'update_records':update_records, 'protocol_version':1, 'client_version':CLIENT_VERSION, '2fa_token':params.mfa_token, '2fa_type':params.mfa_type, 'session_token':params.session_token, 'username':params.user } if not params.session_token: try: login(params) except: if plugin_name: print('Plugin updated password to: ' + new_password) raise payload = make_json(params, update_records) if params.debug: print('payload: ' + str(payload)) try: r = requests.post(params.server, json=payload) except: if plugin_name: print('Plugin updated password to: ' + new_password) raise CommunicationError(sys.exc_info()[0]) response_json = r.json() if params.debug: debug_response(params, payload, r) if response_json['result_code'] == 'auth_failed': if params.debug: print('Re-authorizing.') try: login(params) except: if plugin_name: print('Plugin updated password to: ' + new_password) raise payload = make_json(params, update_records) try: r = requests.post(params.server, json=payload) except: print('Comm error during re-auth') if plugin_name: print('Plugin updated password to: ' + new_password) raise CommunicationError(sys.exc_info()[0]) response_json = r.json() if params.debug: debug_response(params, payload, r) if response_json['result'] == 'success': new_revision = 0 if 'update_records' in response_json: for info in response_json['update_records']: if info['record_uid'] == record_uid: if info['status'] == 'success': # all records in the transaction get the # same revision. this just checks 100% success new_revision = response_json['revision'] if new_revision == 0: print('Error: Revision not updated') if plugin_name: print('Plugin updated password to: ' + new_password) return False if new_revision == new_record['revision']: print('Error: Revision did not change') if plugin_name: print('Plugin updated password to: ' + new_password) return False print('Rotation successful for record_uid=' + \ str(new_record['record_uid']) + ', revision=' + \ str(new_record['revision']), ', new_revision=' + \ str(new_revision)) # update local cache params.record_cache[record_uid]['revision'] = new_revision else : if response_json['result_code']: if plugin_name: print('Plugin updated password to: ' + new_password) raise CommunicationError('Unexpected problem: ' + \ response_json['result_code']) return True