def get_otpauth_url(serial: str, secret: str) -> str: """ Get the OTPAuth URL for the serial/secret pair https://github.com/google/google-authenticator/wiki/Key-Uri-Format """ totp = TOTP(secret, digits=8) return totp.provisioning_uri(serial, issuer_name="Blizzard")
def __enable_otp(self, user): if user.uuid not in self.__settings: self.__settings[user.uuid] = {} user_settings = self.__settings[user.uuid] secret = random_base32() totp = TOTP(secret) user_settings['otp_secret'] = secret self.__save_settings() return totp.provisioning_uri("%s@%s.gosa" % (user.uid, self.env.domain))
def get_secret_qr(totp: TOTP) -> PhotoImage: uri = totp.provisioning_uri(name=WINDOW_TITLE, issuer_name=WINDOW_TITLE) return PhotoImage(qr_make(uri))
def user_settings_otp(ipa, username): addotpform = UserSettingsAddOTPForm(prefix="add-") confirmotpform = UserSettingsConfirmOTPForm(prefix="confirm-") user = User(user_or_404(ipa, username)) secret = None if addotpform.validate_on_submit(): description = addotpform.description.data password = addotpform.password.data if addotpform.otp.data: password += addotpform.otp.data try: maybe_ipa_login(current_app, session, username, password) except python_freeipa.exceptions.InvalidSessionPassword: addotpform.password.errors.append(_("Incorrect password")) else: secret = b32encode(os.urandom(OTP_KEY_LENGTH)).decode('ascii') # Prefill the form for the next step confirmotpform.process( MultiDict({ "confirm-secret": secret, "confirm-description": description })) if confirmotpform.validate_on_submit(): try: ipa.otptoken_add( o_ipatokenowner=username, o_description=confirmotpform.description.data, o_ipatokenotpkey=confirmotpform.secret.data, ) except python_freeipa.exceptions.FreeIPAError as e: current_app.logger.error( f'An error happened while creating an OTP token for user {username}: {e.message}' ) confirmotpform.non_field_errors.errors.append( _('Cannot create the token.')) else: flash(_('The token has been created.'), "success") return redirect(url_for('.user_settings_otp', username=username)) if confirmotpform.is_submitted(): # This form is inside the modal. Keep a value in otp_uri or the modal will not open # to show the errors. secret = confirmotpform.secret.data # Compute the token URI if secret: description = addotpform.description.data or confirmotpform.description.data token = TOTP(secret) otp_uri = token.provisioning_uri(name=description, issuer_name=user.krbname) else: otp_uri = None # List existing tokens tokens = [ OTPToken(t) for t in ipa.otptoken_find(o_ipatokenowner=username)["result"] ] tokens.sort(key=lambda t: t.description or "") return render_template( 'user-settings-otp.html', addotpform=addotpform, confirmotpform=confirmotpform, user=user, activetab="otp", tokens=tokens, otp_uri=otp_uri, )
def build_provisioning_qrc(totp: pyotp.TOTP, user_name: str, issuer_name: str) -> pyqrcode.QRCode: totp_url = totp.provisioning_uri(user_name, issuer_name) return pyqrcode.create(totp_url)
class SafeEntry(SafeElement): # pylint: disable=too-many-instance-attributes, too-many-public-methods _color_key = "color_prop_LcljUMJZ9X" _expired_id: int | None = None _note_key = "Notes" _otp: OTP | None = None _otp_key = "otp" def __init__(self, db_manager: DatabaseManager, entry: Entry) -> None: """GObject to handle a safe entry. :param DatabaseManager db_manager: database of the entry :param Entry entry: entry to handle """ super().__init__(db_manager, entry) self._entry: Entry = entry self._attachments: list[Attachment] = entry.attachments or [] self._attributes: dict[str, str] = { key: value for key, value in entry.custom_properties.items() if key not in (self._color_key, self._note_key, self._otp_key) } color_value: str = entry.get_custom_property(self._color_key) self._color: str = color_value or EntryColor.NONE.value self._icon_nr: str = entry.icon or "" self._password: str = entry.password or "" self._url: str = entry.url or "" self._username: str = entry.username or "" otp_uri = entry.get_custom_property("otp") if otp_uri: try: self._otp = parse_uri(otp_uri) except ValueError as err: logging.debug(err) self._check_expiration() @property def entry(self) -> Entry: """Get entry :returns: entry :rtype: Entry """ return self._entry def duplicate(self): """Duplicate an entry """ title: str = self.name or "" username: str = self.username or "" password: str = self.password or "" # NOTE: With clone is meant a duplicated object, not the process # of cloning/duplication; "the" clone entry = self.entry clone_entry: Entry = self._db_manager.db.add_entry( entry.parentgroup, title + " - " + _("Clone"), username, password, url=entry.url, notes=entry.notes, expiry_time=entry.expiry_time, tags=entry.tags, icon=entry.icon, force_creation=True, ) clone_entry.expires = entry.expires # Add custom properties for key in entry.custom_properties: value: str = entry.custom_properties[key] or "" clone_entry.set_custom_property(key, value) safe_entry = SafeEntry(self._db_manager, clone_entry) self.parentgroup.updated() self._db_manager.entries.append(safe_entry) def _check_expiration(self) -> None: """Check expiration If the entry is expired, this ensures that a notification is sent. If the entry is not expired yet, a timeout is set to regularly check if the entry is expired. """ if self._expired_id: GLib.source_remove(self._expired_id) self._expired_id = None if not self.props.expires: return if self.props.expired: self.notify("expired") else: self._expired_id = GLib.timeout_add_seconds(600, self._is_expired) def _is_expired(self) -> bool: if self.props.expired: self._expired_id = None self.notify("expired") return GLib.SOURCE_REMOVE return GLib.SOURCE_CONTINUE @GObject.Property(type=object, flags=GObject.ParamFlags.READABLE) def attachments(self) -> list[Attachment]: return self._attachments def add_attachment(self, byte_buffer: bytes, filename: str) -> Attachment: """Add an attachment to the entry :param bytes byte_buffer: attachment content :param str filename: attachment name :returns: attachment :rtype: Attachment """ attachment_id = self._db_manager.db.add_binary(byte_buffer) attachment = self._entry.add_attachment(attachment_id, filename) self._attachments.append(attachment) self.updated() return attachment def delete_attachment(self, attachment: Attachment) -> None: """Remove an attachment from the entry :param Attachmennt attachment: attachment to delete """ self._db_manager.db.delete_binary(attachment.id) self._attachments.remove(attachment) self.updated() def get_attachment(self, id_: str) -> Attachment | None: """Get an attachment from its id. :param str id_: attachment id :returns: attachment :rtype: Attachment """ for attachment in self._attachments: if str(attachment.id) == id_: return attachment return None def get_attachment_content(self, attachment: Attachment) -> bytes: """Get an attachment content :param Attachmennt attachment: attachment """ return self._db_manager.db.binaries[attachment.id] @GObject.Property(type=object, flags=GObject.ParamFlags.READABLE) def attributes(self) -> dict[str, str]: return self._attributes def has_attribute(self, key: str) -> bool: """Check if an attribute exists. :param str key: attribute key to check """ return key in self._attributes def set_attribute(self, key: str, value: str) -> None: """Add or replace an entry attribute :param str key: attribute key :param str value: attribute value """ self._entry.set_custom_property(key, value) self._attributes[key] = value self.updated() def delete_attribute(self, key: str) -> None: """Delete an attribute :param key: attribute key to delete """ if not self.has_attribute(key): return self._entry.delete_custom_property(key) self._attributes.pop(key) self.updated() @GObject.Property(type=str, default=EntryColor.NONE.value) def color(self) -> str: """Get entry color :returns: color as string :rtype: str """ return self._color @color.setter # type: ignore def color(self, new_color: str) -> None: """Set an entry color :param str new_color: new color as string """ if new_color != self._color: self._color = new_color self._entry.set_custom_property(self._color_key, new_color) self.updated() @GObject.Property(type=object) def icon(self) -> Icon: """Get icon number :returns: icon number or "0" if no icon :rtype: str """ try: return ICONS[self._icon_nr] except KeyError: return ICONS["0"] @icon.setter # type: ignore def icon(self, new_icon_nr: str) -> None: """Set icon number :param str new_icon_nr: new icon number """ if new_icon_nr != self._icon_nr: self._icon_nr = new_icon_nr self._entry.icon = new_icon_nr self.notify("icon-name") self.updated() @GObject.Property(type=str, default="", flags=GObject.ParamFlags.READABLE) def icon_name(self) -> str: """Get the icon name :returns: icon name or the default icon if undefined :rtype: str """ return self.props.icon.name @GObject.Property(type=str, default="") def otp(self) -> str: if self._otp: return self._otp.secret return "" @otp.setter # type: ignore def otp(self, otp: str) -> None: updated = False # Some sites give the secret in chunks split by spaces for easy reading # lets strip those as they'll produce an invalid secret. otp = otp.replace(" ", "") if not otp and self._otp: # Delete existing self._otp = None self._element.delete_custom_property("otp") self.updated() elif self._otp and self._otp.secret != otp: # Changing an existing OTP self._otp.secret = otp updated = True elif otp: # Creating brand new OTP. self._otp = TOTP(otp, issuer=self.name) updated = True if updated: self._element.set_custom_property("otp", self._otp.provisioning_uri()) self.updated() def otp_interval(self) -> int: if isinstance(self._otp, TOTP): return self._otp.interval return 30 def otp_lifespan(self) -> float | None: """Returns seconds until token expires.""" if isinstance(self._otp, TOTP): gnow = GLib.DateTime.new_now_utc() now_seconds = gnow.to_unix() now_milis = gnow.get_seconds() % 1 now = now_seconds + now_milis return self._otp.interval - now % self._otp.interval return None def otp_token(self): # pylint: disable=inconsistent-return-statements if self._otp: try: # pylint: disable=inconsistent-return-statements return self._otp.now() except binascii.Error: logging.debug( "Error cought in OTP token generation (likely invalid " "base32 secret).") @GObject.Property(type=str, default="") def password(self) -> str: """Get entry password :returns: password or an empty string if there is none :rtype: str """ return self._password @password.setter # type: ignore def password(self, new_password: str) -> None: """Set entry password :param str new_password: new password """ if new_password != self._password: self._password = new_password self._entry.password = new_password self.updated() @GObject.Property(type=str, default="") def url(self) -> str: """Get entry url :returns: url or an empty string if there is none :rtype: str """ return self._url @url.setter # type: ignore def url(self, new_url: str) -> None: """Set entry url :param str new_url: new url """ if new_url != self._url: self._url = new_url self._entry.url = new_url self.updated() @GObject.Property(type=str, default="") def username(self) -> str: """Get entry username :returns: username or an empty string if there is none :rtype: str """ return self._username @username.setter # type: ignore def username(self, new_username: str) -> None: """Set entry username :param str new_username: new username """ if new_username != self._username: self._username = new_username self._entry.username = new_username self.updated() @GObject.Property(type=bool, default=False) def expires(self) -> bool: return self.entry.expires @expires.setter # type: ignore def expires(self, value: bool) -> None: if value != self.entry.expires: self.entry.expires = value self._check_expiration() self.updated() @GObject.Property(type=bool, default=False, flags=GObject.ParamFlags.READABLE | GObject.ParamFlags.EXPLICIT_NOTIFY) def expired(self): return self.entry.expired @property def expiry_time(self) -> GLib.DateTime | None: """Returns the expiration time in the UTC timezone. Returns None when there isn't an expiration date. """ time = self.entry.expiry_time if not time: return None gtime = GLib.DateTime.new_utc(time.year, time.month, time.day, time.hour, time.minute, time.second) return gtime @expiry_time.setter # type: ignore def expiry_time(self, value: GLib.DateTime) -> None: """Sets the expiration time in the UTC timezone.""" if value != self.entry.expiry_time: expired = datetime( value.get_year(), value.get_month(), value.get_day_of_month(), value.get_hour(), value.get_minute(), value.get_second(), tzinfo=timezone.utc, ) self.entry.expiry_time = expired self._check_expiration() self.updated()