Coverage for app/venv/lib/python3.14/site-packages/weblate/vcs/gpg.py: 28%

60 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-07 07:15 +0000

1# Copyright © Michal Čihař <michal@weblate.org> 

2# 

3# SPDX-License-Identifier: GPL-3.0-or-later 

4 

5from __future__ import annotations 

6 

7import subprocess 

8 

9from django.conf import settings 

10from django.core.cache import cache 

11from siphashc import siphash 

12 

13from weblate.trans.util import get_clean_env 

14from weblate.utils.errors import report_error 

15from weblate.utils.files import cleanup_error_message 

16 

17GPG_ERRORS: dict[str, str] = {} 

18 

19 

20def gpg_error(name: str, error: Exception, silent: bool = False) -> None: 

21 report_error(name) 

22 

23 if not silent: 

24 GPG_ERRORS[name] = "{}\n{}\n{}".format( 

25 cleanup_error_message(str(error)), 

26 cleanup_error_message(getattr(error, "stderr", "")), 

27 cleanup_error_message(getattr(error, "stdout", "")), 

28 ) 

29 

30 

31def generate_gpg_key() -> str | None: 

32 try: 

33 subprocess.run( 

34 [ 

35 "gpg", 

36 "--batch", 

37 "--pinentry-mode", 

38 "loopback", 

39 "--passphrase", 

40 "", 

41 "--quick-generate-key", 

42 settings.WEBLATE_GPG_IDENTITY, 

43 settings.WEBLATE_GPG_ALGO, 

44 "default", 

45 "never", 

46 ], 

47 env=get_clean_env(), 

48 capture_output=True, 

49 text=True, 

50 check=True, 

51 ) 

52 except (subprocess.CalledProcessError, OSError) as error: 

53 gpg_error("GPG key generating", error) 

54 return None 

55 return get_gpg_key() 

56 

57 

58def get_gpg_key(silent=False) -> str | None: 

59 try: 

60 result = subprocess.run( 

61 [ 

62 "gpg", 

63 "--batch", 

64 "--with-colons", 

65 "--list-secret-keys", 

66 settings.WEBLATE_GPG_IDENTITY, 

67 ], 

68 capture_output=True, 

69 env=get_clean_env(), 

70 text=True, 

71 check=True, 

72 ) 

73 except (subprocess.CalledProcessError, OSError) as error: 

74 gpg_error("GPG key listing", error, silent) 

75 return None 

76 for line in result.stdout.splitlines(): 

77 if not line.startswith("fpr:"): # codespell:ignore fpr 

78 continue 

79 return line.split(":")[9] 

80 return None 

81 

82 

83def gpg_cache_key(suffix: str) -> str: 

84 return "gpg:{}:{}".format( 

85 siphash("Weblate GPG hash", settings.WEBLATE_GPG_IDENTITY), suffix 

86 ) 

87 

88 

89def get_gpg_sign_key() -> str | None: 

90 """High level wrapper to cache key ID.""" 

91 if not settings.WEBLATE_GPG_IDENTITY: 91 ↛ 93line 91 didn't jump to line 93 because the condition on line 91 was always true

92 return None 

93 cache_key = gpg_cache_key("id") 

94 keyid = cache.get(cache_key) 

95 if keyid is None: 

96 keyid = get_gpg_key(silent=True) 

97 if keyid is None: 

98 keyid = generate_gpg_key() 

99 if keyid: 

100 cache.set(cache_key, keyid, 7 * 86400) 

101 return keyid 

102 

103 

104def get_gpg_public_key() -> str | None: 

105 key = get_gpg_sign_key() 

106 if key is None: 106 ↛ 108line 106 didn't jump to line 108 because the condition on line 106 was always true

107 return None 

108 cache_key = gpg_cache_key("public") 

109 data = cache.get(cache_key) 

110 if not data: 

111 try: 

112 result = subprocess.run( 

113 ["gpg", "--batch", "-armor", "--export", key], 

114 env=get_clean_env(), 

115 capture_output=True, 

116 text=True, 

117 check=True, 

118 ) 

119 except (subprocess.CalledProcessError, OSError) as error: 

120 gpg_error("GPG key public", error) 

121 return None 

122 data = result.stdout 

123 cache.set(cache_key, data, 7 * 86400) 

124 return data