KeyringAttribute.py 3.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  1. # Copyright (c) 2021 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. from typing import Type, TYPE_CHECKING, Optional, List
  4. import keyring
  5. from keyring.backend import KeyringBackend
  6. from keyring.errors import NoKeyringError, PasswordSetError, KeyringLocked
  7. from UM.Logger import Logger
  8. if TYPE_CHECKING:
  9. from cura.OAuth2.Models import BaseModel
  10. # Need to do some extra workarounds on windows:
  11. import sys
  12. from UM.Platform import Platform
  13. if Platform.isWindows():
  14. import win32timezone
  15. from keyring.backends.Windows import WinVaultKeyring
  16. keyring.set_keyring(WinVaultKeyring())
  17. if Platform.isOSX():
  18. from keyring.backends.macOS import Keyring
  19. keyring.set_keyring(Keyring())
  20. if Platform.isLinux():
  21. # We do not support the keyring on Linux, so make sure no Keyring backend is loaded, even if there is a system one.
  22. from keyring.backends.fail import Keyring as NoKeyringBackend
  23. keyring.set_keyring(NoKeyringBackend())
  24. # Even if errors happen, we don't want this stored locally:
  25. DONT_EVER_STORE_LOCALLY: List[str] = ["refresh_token"]
  26. class KeyringAttribute:
  27. """
  28. Descriptor for attributes that need to be stored in the keyring. With Fallback behaviour to the preference cfg file
  29. """
  30. def __get__(self, instance: "BaseModel", owner: type) -> Optional[str]:
  31. if self._store_secure: # type: ignore
  32. try:
  33. value = keyring.get_password("cura", self._keyring_name)
  34. return value if value != "" else None
  35. except NoKeyringError:
  36. self._store_secure = False
  37. Logger.logException("w", "No keyring backend present")
  38. return getattr(instance, self._name)
  39. except KeyringLocked:
  40. self._store_secure = False
  41. Logger.log("i", "Access to the keyring was denied.")
  42. return getattr(instance, self._name)
  43. except UnicodeDecodeError:
  44. self._store_secure = False
  45. Logger.log("w", "The password retrieved from the keyring cannot be used because it contains characters that cannot be decoded.")
  46. return getattr(instance, self._name)
  47. else:
  48. return getattr(instance, self._name)
  49. def __set__(self, instance: "BaseModel", value: Optional[str]):
  50. if self._store_secure:
  51. setattr(instance, self._name, None)
  52. if value is not None:
  53. try:
  54. keyring.set_password("cura", self._keyring_name, value)
  55. except (PasswordSetError, KeyringLocked):
  56. self._store_secure = False
  57. if self._name not in DONT_EVER_STORE_LOCALLY:
  58. setattr(instance, self._name, value)
  59. Logger.logException("w", "Keyring access denied")
  60. except NoKeyringError:
  61. self._store_secure = False
  62. if self._name not in DONT_EVER_STORE_LOCALLY:
  63. setattr(instance, self._name, value)
  64. Logger.logException("w", "No keyring backend present")
  65. except BaseException as e:
  66. # A BaseException can occur in Windows when the keyring attempts to write a token longer than 1024
  67. # characters in the Windows Credentials Manager.
  68. self._store_secure = False
  69. if self._name not in DONT_EVER_STORE_LOCALLY:
  70. setattr(instance, self._name, value)
  71. Logger.log("w", "Keyring failed: {}".format(e))
  72. else:
  73. setattr(instance, self._name, value)
  74. def __set_name__(self, owner: type, name: str):
  75. self._name = "_{}".format(name)
  76. self._keyring_name = name
  77. self._store_secure = False
  78. try:
  79. self._store_secure = KeyringBackend.viable
  80. except NoKeyringError:
  81. Logger.logException("w", "Could not use keyring")
  82. setattr(owner, self._name, None)