def __init__(self, cfg): sec_level = cfg.collectd_security_level if sec_level in ("sign", "SIGN", "Sign", 1): self.sec_level = 1 elif sec_level in ("encrypt", "ENCRYPT", "Encrypt", 2): self.sec_level = 2 else: self.sec_level = 0 self.auth_file = cfg.collectd_auth_file self.auth_db = {} self.cfg_mon = None if self.auth_file: self.load_auth_file() self.cfg_mon = FileMonitor(self.auth_file) if self.sec_level: if not self.auth_file: raise ConfigError("Collectd security level configured but no " "auth file specified in configuration") if not self.auth_db: log.warning("Collectd security level configured but no " "user/passwd entries loaded from auth file")
class CollectDCrypto(object): def __init__(self, cfg): sec_level = cfg.collectd_security_level if sec_level in ("sign", "SIGN", "Sign", 1): self.sec_level = 1 elif sec_level in ("encrypt", "ENCRYPT", "Encrypt", 2): self.sec_level = 2 else: self.sec_level = 0 self.auth_file = cfg.collectd_auth_file self.auth_db = {} self.cfg_mon = None self.crypto_backend = default_backend() if self.auth_file: self.load_auth_file() self.cfg_mon = FileMonitor(self.auth_file) if self.sec_level: if not self.auth_file: raise ConfigError("Collectd security level configured but no " "auth file specified in configuration") if not self.auth_db: log.warning("Collectd security level configured but no " "user/passwd entries loaded from auth file") def load_auth_file(self): try: f = open(self.auth_file) except IOError as exc: raise ConfigError("Unable to load collectd's auth file: %r" % exc) self.auth_db.clear() for line in f: line = line.strip() if not line or line[0] == "#": continue user, passwd = line.split(":", 1) user = user.strip() passwd = passwd.strip() if not user or not passwd: log.warning("Found line with missing user or password") continue if user in self.auth_db: log.warning("Found multiple entries for single user") self.auth_db[user] = passwd f.close() log.info("Loaded collectd's auth file from %s", self.auth_file) def parse(self, data): if len(data) < 4: raise ProtocolError("Truncated header.") part_type, part_len = struct.unpack("!HH", data[:4]) sec_level = {0x0200: 1, 0x0210: 2}.get(part_type, 0) if sec_level < self.sec_level: raise ProtocolError("Packet has lower security level than allowed") if not sec_level: return data if sec_level == 1 and not self.sec_level: return data[part_len:] data = data[4:] part_len -= 4 if len(data) < part_len: raise ProtocolError("Truncated part payload.") if self.cfg_mon is not None and self.cfg_mon.modified(): log.info("Collectd authfile modified, reloading") self.load_auth_file() if sec_level == 1: return self.parse_signed(part_len, data) if sec_level == 2: return self.parse_encrypted(part_len, data) def parse_signed(self, part_len, data): if part_len <= 32: raise ProtocolError("Truncated signed part.") sig, data = data[:32], data[32:] uname_len = part_len - 32 uname = data[:uname_len].decode() if uname not in self.auth_db: raise ProtocolError("Signed packet, unknown user '%s'" % uname) password = self.auth_db[uname].encode() sig2 = hmac.new(password, msg=data, digestmod=sha256).digest() if not self._hashes_match(sig, sig2): raise ProtocolError("Bad signature from user '%s'" % uname) data = data[uname_len:] return data def parse_encrypted(self, part_len, data): if part_len != len(data): raise ProtocolError("Enc pkt size disaggrees with header.") if len(data) <= 38: raise ProtocolError("Truncated encrypted part.") uname_len, data = struct.unpack("!H", data[:2])[0], data[2:] if len(data) <= uname_len + 36: raise ProtocolError("Truncated encrypted part.") uname, data = data[:uname_len].decode(), data[uname_len:] if uname not in self.auth_db: raise ProtocolError("Couldn't decrypt, unknown user '%s'" % uname) iv, data = data[:16], data[16:] password = self.auth_db[uname].encode() key = sha256(password).digest() pad_bytes = 16 - (len(data) % 16) data += b'\0' * pad_bytes cipher = Cipher(algorithms.AES(key), modes.OFB(iv), backend=self.crypto_backend) decryptor = cipher.decryptor() data = decryptor.update(data) data = data[:-pad_bytes] tag, data = data[:20], data[20:] tag2 = sha1(data).digest() if not self._hashes_match(tag, tag2): raise ProtocolError("Bad checksum on enc pkt for '%s'" % uname) return data def _hashes_match(self, a, b): """Constant time comparison of bytes for py3, strings for py2""" if len(a) != len(b): return False diff = 0 if six.PY2: a = bytearray(a) b = bytearray(b) for x, y in zip(a, b): diff |= x ^ y return not diff