# # SPDX-FileCopyrightText: 2021-2026 Sébastien Helleu # # SPDX-License-Identifier: GPL-3.0-or-later # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # Prevent a password from being accidentally sent to a buffer # (requires WeeChat ≥ 0.4.0 and WeeChat ≥ 3.1 to check secured data). # """Anti password script.""" import re IMPORT_OK = True try: import weechat # type: ignore[import] except ImportError: print("This script must be run under WeeChat.") print("Get WeeChat now at: https://weechat.org/") IMPORT_OK = False SCRIPT_NAME = "anti_password" SCRIPT_AUTHOR = "Sébastien Helleu " SCRIPT_VERSION = "1.2.2" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Prevent a password from being accidentally sent to a buffer" # script options AP_SETTINGS_DEFAULT = { "allowed_regex": { "default": "^(http|https|ftp|file|irc)://", "help": ( "allowed regular expression (case is ignored); this is checked " "first: if the input matched this regular expression, it is " "considered safe and sent immediately to the buffer; " "if empty, nothing specific is allowed" ), }, "password_condition": { "default": ( "${words} == 1 && ${lower} >= 1 && ${upper} >= 1 " "&& ${digits} >= 1 && ${special} >= 1" ), "help": ( "condition evaluated to check if the input string is a password; " "allowed variables: " "${words} = number of words, " "${lower} = number of lower case letters, " "${upper} = number of upper case letters, " "${digits} = number of digits, " "${special} = number of other chars (not letter/digits/spaces), " ), }, "check_secured_data": { "default": "equal", "help": ( "consider that all secured data values are passwords and can not " "be sent to buffers, possible values: " "off = do not check secured data at all, " "equal = reject input if stripped input is equal to a secured " "data value, " "include = reject input if a secured data value is part of input" ), }, "max_rejects": { "default": "3", "help": ( "max number of rejects for a given input text; if you press Enter " "more than N times with exactly the same input, the text is " "finally sent to the buffer; " "if set to 0, the input is never sent to the buffer when it is " "considered harmful (be careful, according to the other settings, " "this can completely block the input)" ), }, } ap_settings = {} ap_reject = { "input": "", "count": 0, } def ap_config_cb(_data: str, option: str, value: str) -> int: """Called when a script option is changed.""" pos = option.rfind(".") if pos > 0: name = option[pos + 1 :] if name in ap_settings: ap_settings[name] = value return weechat.WEECHAT_RC_OK def ap_input_is_secured_data(input_text: str) -> bool: """Check if input_text is any value of a secured data.""" check = ap_settings["check_secured_data"] if check == "off": return False sec_data = weechat.info_get_hashtable("secured_data", {}) or {} if check == "include": for value in sec_data.values(): if value and value in input_text: return True if check == "equal": return input_text.strip() in sec_data.values() return False def ap_input_matches_condition(input_text: str) -> bool: """Check if input_text matches the password condition.""" # count chars in the input text words = len(list(filter(None, re.split(r"\s+", input_text)))) lower = sum(1 for c in input_text if c.islower()) upper = sum(1 for c in input_text if c.isupper()) digits = sum(1 for c in input_text if c.isdigit()) special = sum(1 for c in input_text if not (c.isalnum() or c.isspace())) # evaluate password condition extra_vars = { "words": str(words), "lower": str(lower), "upper": str(upper), "digits": str(digits), "special": str(special), } ret = weechat.string_eval_expression( ap_settings["password_condition"], {}, extra_vars, {"type": "condition"}, ) return ret == "1" def ap_input_is_password(input_text: str) -> bool: """Check if input_text looks like a password.""" return ap_input_is_secured_data(input_text) or ap_input_matches_condition( input_text ) def ap_input_return_cb(_data: str, buf: str, _command: str) -> int: """Callback called when Return key is pressed in a buffer.""" try: max_rejects = int(ap_settings["max_rejects"]) except ValueError: max_rejects = 3 input_text = weechat.buffer_get_string(buf, "input") # check if input is a command if not weechat.string_input_for_buffer(input_text): # commands are ignored ap_reject["input"] = "" ap_reject["count"] = 0 return weechat.WEECHAT_RC_OK # check if input matches the allowed regex regex = ap_settings["allowed_regex"] if regex and re.search(regex, input_text, re.IGNORECASE): # allowed regex ap_reject["input"] = "" ap_reject["count"] = 0 return weechat.WEECHAT_RC_OK if ap_input_is_password(input_text): if ap_reject["input"] == input_text: if ap_reject["count"] >= max_rejects > 0: # it looks like a password but send anyway after N rejects ap_reject["input"] = "" ap_reject["count"] = 0 return weechat.WEECHAT_RC_OK ap_reject["count"] += 1 else: ap_reject["input"] = input_text ap_reject["count"] = 1 # password detected, do NOT send it to the buffer! return weechat.WEECHAT_RC_OK_EAT # not a password ap_reject["input"] = "" ap_reject["count"] = 0 return weechat.WEECHAT_RC_OK def main() -> None: """Main function.""" if weechat.register( SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", "" ): # set default settings for name, option in AP_SETTINGS_DEFAULT.items(): if weechat.config_is_set_plugin(name): ap_settings[name] = weechat.config_get_plugin(name) else: weechat.config_set_plugin(name, option["default"]) ap_settings[name] = option["default"] weechat.config_set_desc_plugin( name, f'{option["help"]} (default: "{option["default"]}")' ) # detect config changes weechat.hook_config(f"plugins.var.python.{SCRIPT_NAME}.*", "ap_config_cb", "") # hook Return key weechat.hook_command_run("/input return", "ap_input_return_cb", "") if __name__ == "__main__" and IMPORT_OK: main()