Make debug and info messages from exporter subcommands translatable
Marco Ricci

Marco Ricci commited on 2025-01-11 16:28:48
Zeige 3 geänderte Dateien mit 619 Einfügungen und 135 Löschungen.


We add all debug and info messages from the `derivepassphrase export
vault` subcommand to the translatable strings enums.  We also improve
several debug messages from the "inline calculation" style to the more
easily translatable "tabular listing of relevant data" style.

Two unimportant info messages were dropped:

  * Attempting to parse as v0.2 configuration
  * Attempting to parse as v0.3 configuration
... ...
@@ -152,6 +152,7 @@ class TranslatedString:
152 152
             str
153 153
             | TranslatableString
154 154
             | Label
155
+            | DebugMsgTemplate
155 156
             | InfoMsgTemplate
156 157
             | WarnMsgTemplate
157 158
             | ErrMsgTemplate
... ...
@@ -161,7 +162,7 @@ class TranslatedString:
161 162
         **kwargs: Any,  # noqa: ANN401
162 163
     ) -> None:
163 164
         if isinstance(
164
-            template, (Label, InfoMsgTemplate, WarnMsgTemplate, ErrMsgTemplate)
165
+            template, (Label, DebugMsgTemplate, InfoMsgTemplate, WarnMsgTemplate, ErrMsgTemplate)
165 166
         ):
166 167
             template = cast('TranslatableString', template.value)
167 168
         self.template = template
... ...
@@ -781,7 +782,376 @@ class Label(enum.Enum):
781 782
     )
782 783
 
783 784
 
785
+class DebugMsgTemplate(enum.Enum):
786
+    BUCKET_ITEM_FOUND = _prepare_translatable(
787
+        comments=r"""
788
+        TRANSLATORS: This message is emitted by the vault configuration
789
+        exporter for "storeroom"-type configuration directories.  The
790
+        system stores entries in different "buckets" of a hash table.
791
+        Here, we report on a single item (path and value) we discovered
792
+        after decrypting the whole bucket.  (We ensure the path and
793
+        value are printable as-is.)
794
+        """,
795
+        msg='Found bucket item: {path} -> {value}',
796
+        context='debug message',
797
+        flags='python-brace-format',
798
+    )
799
+    DECRYPT_BUCKET_ITEM_INFO = _prepare_translatable(
800
+        comments=r"""
801
+        TRANSLATORS: "AES256-CBC" and "PKCS#7" are, in essence, names of
802
+        formats, and should not be translated.  "IV" means
803
+        "initialization vector", and is specifically a cryptographic
804
+        term, as are "plaintext" and "ciphertext".
805
+        """,
806
+        msg="""
807
+        Decrypt bucket item contents:
808
+
809
+          \b
810
+          Encryption key (master key): {enc_key}
811
+          Encryption cipher: AES256-CBC with PKCS#7 padding
812
+          Encryption IV: {iv}
813
+          Encrypted ciphertext: {ciphertext}
814
+          Plaintext: {plaintext}
815
+        """,
816
+        context='debug message',
817
+        flags='python-brace-format',
818
+    )
819
+    DECRYPT_BUCKET_ITEM_KEY_INFO = _prepare_translatable(
820
+        msg="""
821
+        Decrypt bucket item:
822
+
823
+          \b
824
+          Plaintext: {plaintext}
825
+          Encryption key (master key): {enc_key}
826
+          Signing key (master key): {sign_key}
827
+        """,
828
+        comments='',
829
+        context='debug message',
830
+        flags='python-brace-format',
831
+    )
832
+    DECRYPT_BUCKET_ITEM_MAC_INFO = _prepare_translatable(
833
+        comments=r"""
834
+        TRANSLATORS: The MAC stands for "message authentication code",
835
+        which guarantees the authenticity of the message to anyone who
836
+        holds the corresponding key, similar to a digital signature.
837
+        The acronym "MAC" is assumed to be well-known to the English
838
+        target audience, or at least discoverable by them; they *are*
839
+        asking for debug output, after all.  Please use your judgement
840
+        as to whether to translate this term or not, expanded or not.
841
+        """,
842
+        msg="""
843
+        Decrypt bucket item contents:
844
+
845
+          \b
846
+          MAC key: {sign_key}
847
+          Authenticated content: {ciphertext}
848
+          Claimed MAC value: {claimed_mac}
849
+          Computed MAC value: {actual_mac}
850
+        """,
851
+        context='debug message',
852
+        flags='python-brace-format',
853
+    )
854
+    DECRYPT_BUCKET_ITEM_SESSION_KEYS_INFO = _prepare_translatable(
855
+        comments=r"""
856
+        TRANSLATORS: "AES256-CBC" and "PKCS#7" are, in essence, names of
857
+        formats, and should not be translated.  "IV" means
858
+        "initialization vector", and is specifically a cryptographic
859
+        term, as are "plaintext" and "ciphertext".
860
+        """,
861
+        msg="""
862
+        Decrypt bucket item session keys:
863
+
864
+          \b
865
+          Encryption key (master key): {enc_key}
866
+          Encryption cipher: AES256-CBC with PKCS#7 padding
867
+          Encryption IV: {iv}
868
+          Encrypted ciphertext: {ciphertext}
869
+          Plaintext: {plaintext}
870
+          Parsed plaintext: {code}
871
+        """,
872
+        context='debug message',
873
+        flags='python-brace-format',
874
+    )
875
+    DECRYPT_BUCKET_ITEM_SESSION_KEYS_MAC_INFO = _prepare_translatable(
876
+        comments=r"""
877
+        TRANSLATORS: The MAC stands for "message authentication code",
878
+        which guarantees the authenticity of the message to anyone who
879
+        holds the corresponding key, similar to a digital signature.
880
+        The acronym "MAC" is assumed to be well-known to the English
881
+        target audience, or at least discoverable by them; they *are*
882
+        asking for debug output, after all.  Please use your judgement
883
+        as to whether to translate this term or not, expanded or not.
884
+        """,
885
+        msg="""
886
+        Decrypt bucket item session keys:
887
+
888
+          \b
889
+          MAC key (master key): {sign_key}
890
+          Authenticated content: {ciphertext}
891
+          Claimed MAC value: {claimed_mac}
892
+          Computed MAC value: {actual_mac}
893
+        """,
894
+        context='debug message',
895
+        flags='python-brace-format',
896
+    )
897
+    DERIVED_MASTER_KEYS_KEYS = _prepare_translatable(
898
+        msg="""
899
+        Derived master keys' keys:
900
+
901
+          \b
902
+          Encryption key: {enc_key}
903
+          Signing key: {sign_key}
904
+          Password: {pw_bytes}
905
+          Function call: pbkdf2(algorithm={algorithm!r}, length={length!r}, salt={salt!r}, iterations={iterations!r})
906
+
907
+        """,  # noqa: E501
908
+        comments='',
909
+        context='debug message',
910
+        flags='python-brace-format',
911
+    )
912
+    DIRECTORY_CONTENTS_CHECK_OK = _prepare_translatable(
913
+        comments=r"""
914
+        TRANSLATORS: This message is emitted by the vault configuration
915
+        exporter for "storeroom"-type configuration directories, while
916
+        "assembling" the items stored in the configuration according to
917
+        the item's "path".  Each "directory" in the path contains a list
918
+        of children it claims to contain, and this list must be matched
919
+        against the actual discovered items.  Now, at the end, we
920
+        actually confirm the claim.  (We would have already thrown an
921
+        error here otherwise.)
922
+        """,
923
+        msg='Directory contents check OK: {path} -> {contents}',
924
+        context='debug message',
925
+        flags='python-brace-format',
926
+    )
927
+    MASTER_KEYS_DATA_MAC_INFO = _prepare_translatable(
928
+        comments=r"""
929
+        TRANSLATORS: The MAC stands for "message authentication code",
930
+        which guarantees the authenticity of the message to anyone who
931
+        holds the corresponding key, similar to a digital signature.
932
+        The acronym "MAC" is assumed to be well-known to the English
933
+        target audience, or at least discoverable by them; they *are*
934
+        asking for debug output, after all.  Please use your judgement
935
+        as to whether to translate this term or not, expanded or not.
936
+        """,
937
+        msg="""
938
+        Master keys data:
939
+
940
+          \b
941
+          MAC key: {sign_key}
942
+          Authenticated content: {ciphertext}
943
+          Claimed MAC value: {claimed_mac}
944
+          Computed MAC value: {actual_mac}
945
+        """,
946
+        context='debug message',
947
+        flags='python-brace-format',
948
+    )
949
+    POSTPONING_DIRECTORY_CONTENTS_CHECK = _prepare_translatable(
950
+        comments=r"""
951
+        TRANSLATORS: This message is emitted by the vault configuration
952
+        exporter for "storeroom"-type configuration directories, while
953
+        "assembling" the items stored in the configuration according to
954
+        the item's "path".  Each "directory" in the path contains a list
955
+        of children it claims to contain, and this list must be matched
956
+        against the actual discovered items.  When emitting this
957
+        message, we merely indicate that we saved the "claimed" list for
958
+        this directory for later.
959
+        """,
960
+        msg='Postponing directory contents check: {path} -> {contents}',
961
+        context='debug message',
962
+        flags='python-brace-format',
963
+    )
964
+    SETTING_CONFIG_STRUCTURE_CONTENTS = _prepare_translatable(
965
+        comments=r"""
966
+        TRANSLATORS: This message is emitted by the vault configuration
967
+        exporter for "storeroom"-type configuration directories, while
968
+        "assembling" the items stored in the configuration according to
969
+        the item's "path".  We confirm that we set the entry at the
970
+        given path to the given value.
971
+        """,
972
+        msg='Setting contents: {path} -> {value}',
973
+        context='debug message',
974
+        flags='python-brace-format',
975
+    )
976
+    SETTING_CONFIG_STRUCTURE_CONTENTS_EMPTY_DIRECTORY = _prepare_translatable(
977
+        comments=r"""
978
+        TRANSLATORS: This message is emitted by the vault configuration
979
+        exporter for "storeroom"-type configuration directories, while
980
+        "assembling" the items stored in the configuration according to
981
+        the item's "path".  We confirm that we set up a currently empty
982
+        directory at the given path.
983
+        """,
984
+        msg='Setting contents (empty directory): {path}',
985
+        context='debug message',
986
+        flags='python-brace-format',
987
+    )
988
+    VAULT_NATIVE_EVP_BYTESTOKEY_INIT = _prepare_translatable(
989
+        comments=r"""
990
+        TRANSLATORS: This message is emitted by the vault configuration
991
+        exporter for "native"-type configuration directories: in v0.2,
992
+        the non-standard and deprecated "EVP_bytestokey" function from
993
+        OpenSSL must be reimplemented from scratch.  The terms "salt"
994
+        and "IV" (initialization vector) are cryptographic terms.
995
+        """,
996
+        msg="""
997
+        evp_bytestokey_md5 (initialization):
998
+
999
+          \b
1000
+          Input: {data}
1001
+          Salt: {salt}
1002
+          Key size: {key_size}
1003
+          IV size: {iv_size}
1004
+          Buffer length: {buffer_length}
1005
+          Buffer: {buffer}
1006
+        """,
1007
+        context='debug message',
1008
+        flags='python-brace-format',
1009
+    )
1010
+    VAULT_NATIVE_EVP_BYTESTOKEY_RESULT = _prepare_translatable(
1011
+        comments=r"""
1012
+        TRANSLATORS: This message is emitted by the vault configuration
1013
+        exporter for "native"-type configuration directories: in v0.2,
1014
+        the non-standard and deprecated "EVP_bytestokey" function from
1015
+        OpenSSL must be reimplemented from scratch.  The terms "salt"
1016
+        and "IV" (initialization vector) are cryptographic terms.
1017
+        This function reports on the updated buffer length and contents
1018
+        after executing one round of hashing.
1019
+        """,
1020
+        msg="""
1021
+        evp_bytestokey_md5 (result):
1022
+
1023
+          \b
1024
+          Encryption key: {enc_key}
1025
+          IV: {iv}
1026
+        """,
1027
+        context='debug message',
1028
+        flags='python-brace-format',
1029
+    )
1030
+    VAULT_NATIVE_EVP_BYTESTOKEY_ROUND = _prepare_translatable(
1031
+        comments=r"""
1032
+        TRANSLATORS: This message is emitted by the vault configuration
1033
+        exporter for "native"-type configuration directories: in v0.2,
1034
+        the non-standard and deprecated "EVP_bytestokey" function from
1035
+        OpenSSL must be reimplemented from scratch.  The terms "salt"
1036
+        and "IV" (initialization vector) are cryptographic terms.
1037
+        This function reports on the updated buffer length and contents
1038
+        after executing one round of hashing.
1039
+        """,
1040
+        msg="""
1041
+        evp_bytestokey_md5 (round update):
1042
+
1043
+          \b
1044
+          Buffer length: {buffer_length}
1045
+          Buffer: {buffer}
1046
+        """,
1047
+        context='debug message',
1048
+        flags='python-brace-format',
1049
+    )
1050
+    VAULT_NATIVE_CHECKING_MAC_DETAILS = _prepare_translatable(
1051
+        comments=r"""
1052
+        TRANSLATORS: This message is emitted by the vault configuration
1053
+        exporter for "native"-type configuration directories.  It is
1054
+        preceded by the info message PARSING_IV_PAYLOAD_MAC; see the
1055
+        commentary there concerning the terms and thoughts on
1056
+        translating them.
1057
+        """,
1058
+        msg="""
1059
+        MAC details:
1060
+
1061
+          \b
1062
+          MAC input: {mac_input}
1063
+          Expected MAC: {mac}
1064
+        """,
1065
+        context='debug message',
1066
+        flags='python-brace-format',
1067
+    )
1068
+    VAULT_NATIVE_PADDED_PLAINTEXT = _prepare_translatable(
1069
+        comments=r"""
1070
+        TRANSLATORS: This message is emitted by the vault configuration
1071
+        exporter for "native"-type configuration directories.  "padding"
1072
+        and "plaintext" are cryptographic terms.
1073
+        """,
1074
+        msg='Padded plaintext: {contents}',
1075
+        context='debug message',
1076
+        flags='python-brace-format',
1077
+    )
1078
+    VAULT_NATIVE_PARSE_BUFFER = _prepare_translatable(
1079
+        comments=r"""
1080
+        TRANSLATORS: This message is emitted by the vault configuration
1081
+        exporter for "native"-type configuration directories.  It is
1082
+        preceded by the info message PARSING_IV_PAYLOAD_MAC; see the
1083
+        commentary there concerning the terms and thoughts on
1084
+        translating them.
1085
+        """,
1086
+        msg="""
1087
+        Buffer: {contents}
1088
+
1089
+          \b
1090
+          IV: {iv}
1091
+          Payload: {payload}
1092
+          MAC: {mac}
1093
+        """,
1094
+        context='debug message',
1095
+        flags='python-brace-format',
1096
+    )
1097
+    VAULT_NATIVE_PLAINTEXT = _prepare_translatable(
1098
+        comments=r"""
1099
+        TRANSLATORS: This message is emitted by the vault configuration
1100
+        exporter for "native"-type configuration directories.
1101
+        "plaintext" is a cryptographic term.
1102
+        """,
1103
+        msg='Plaintext: {contents}',
1104
+        context='debug message',
1105
+        flags='python-brace-format',
1106
+    )
1107
+    VAULT_NATIVE_PBKDF2_CALL = _prepare_translatable(
1108
+        msg="""
1109
+        Master key derivation:
1110
+
1111
+          \b
1112
+          PBKDF2 call: PBKDF2-HMAC(password={password!r}, salt={salt!r}, iterations={iterations!r}, key_size={key_size!r}, algorithm={algorithm!r})
1113
+          Result (binary): {raw_result}
1114
+          Result (hex key): {result_key!r}
1115
+        """,  # noqa: E501
1116
+        comments='',
1117
+        context='debug message',
1118
+        flags='python-brace-format',
1119
+    )
1120
+    VAULT_NATIVE_V02_PAYLOAD_MAC_POSTPROCESSING = _prepare_translatable(
1121
+        comments=r"""
1122
+        TRANSLATORS: This message is emitted by the vault configuration
1123
+        exporter for "native"-type configuration directories.  It is
1124
+        preceded by the info message PARSING_IV_PAYLOAD_MAC and the
1125
+        debug message PARSING_NATIVE_PARSE_BUFFER; see the commentary
1126
+        there concerning the terms and thoughts on translating them.
1127
+        """,
1128
+        msg="""
1129
+        Postprocessing buffer (v0.2):
1130
+
1131
+          \b
1132
+          Payload: {payload} (decoded from base64)
1133
+          MAC: {mac} (decoded from hex)
1134
+        """,
1135
+        context='debug message',
1136
+        flags='python-brace-format',
1137
+    )
1138
+
1139
+
784 1140
 class InfoMsgTemplate(enum.Enum):
1141
+    ASSEMBLING_CONFIG_STRUCTURE = _prepare_translatable(
1142
+        comments=r"""
1143
+        TRANSLATORS: This message is emitted by the vault configuration
1144
+        exporter for "storeroom"-type configuration directories.  The
1145
+        system stores entries in different "buckets" of a hash table.
1146
+        After the respective items in the buckets have been decrypted,
1147
+        we then have a list of item paths plus contents to populate.
1148
+        This must be done in a certain order (we don't yet have an
1149
+        existing directory tree to rely on, but rather must build it
1150
+        on-the-fly), hence the term "assembling".
1151
+        """,
1152
+        msg='Assembling config structure',
1153
+        context='info message',
1154
+    )
785 1155
     CANNOT_LOAD_AS_VAULT_CONFIG = _prepare_translatable(
786 1156
         comments=r"""
787 1157
         TRANSLATORS: "fmt" is a string such as "v0.2" or "storeroom",
... ...
@@ -792,6 +1162,40 @@ class InfoMsgTemplate(enum.Enum):
792 1162
         context='info message',
793 1163
         flags='python-brace-format',
794 1164
     )
1165
+    CHECKING_CONFIG_STRUCTURE_CONSISTENCY = _prepare_translatable(
1166
+        comments=r"""
1167
+        TRANSLATORS: This message is emitted by the vault configuration
1168
+        exporter for "storeroom"-type configuration directories.  Having
1169
+        "assembled" the configuration items according to their claimed
1170
+        paths and contents, we then check if the assembled structure is
1171
+        internally consistent.
1172
+        """,
1173
+        msg='Checking config structure consistency',
1174
+        context='info message',
1175
+    )
1176
+    DECRYPTING_BUCKET = _prepare_translatable(
1177
+        comments=r"""
1178
+        TRANSLATORS: This message is emitted by the vault configuration
1179
+        exporter for "storeroom"-type configuration directories.  The
1180
+        system stores entries in different "buckets" of a hash table.
1181
+        We parse the directory bucket by bucket.  All buckets are
1182
+        numbered in hexadecimal, and typically there are 32 buckets, so
1183
+        2-digit hex numbers.
1184
+        """,
1185
+        msg='Decrypting bucket {bucket_number}',
1186
+        context='info message',
1187
+        flags='python-brace-format',
1188
+    )
1189
+    PARSING_MASTER_KEYS_DATA = _prepare_translatable(
1190
+        comments=r"""
1191
+        TRANSLATORS: This message is emitted by the vault configuration
1192
+        exporter for "storeroom"-type configuration directories.
1193
+        `.keys` is a filename, from which data about the master keys for
1194
+        this configuration are loaded.
1195
+        """,
1196
+        msg='Parsing master keys data from .keys',
1197
+        context='info message',
1198
+    )
795 1199
     PIP_INSTALL_EXTRA = _prepare_translatable(
796 1200
         comments=r"""
797 1201
         TRANSLATORS: This message immediately follows an error message
... ...
@@ -815,6 +1219,36 @@ class InfoMsgTemplate(enum.Enum):
815 1219
         context='info message',
816 1220
         flags='python-brace-format',
817 1221
     )
1222
+    VAULT_NATIVE_CHECKING_MAC = _prepare_translatable(
1223
+        msg='Checking MAC',
1224
+        comments='',
1225
+        context='info message',
1226
+    )
1227
+    VAULT_NATIVE_DECRYPTING_CONTENTS = _prepare_translatable(
1228
+        msg='Decrypting contents',
1229
+        comments='',
1230
+        context='info message',
1231
+    )
1232
+    VAULT_NATIVE_DERIVING_KEYS = _prepare_translatable(
1233
+        msg='Deriving an encryption and signing key',
1234
+        comments='',
1235
+        context='info message',
1236
+    )
1237
+    VAULT_NATIVE_PARSING_IV_PAYLOAD_MAC = _prepare_translatable(
1238
+        comments=r"""
1239
+        TRANSLATORS: This message is emitted by the vault configuration
1240
+        exporter for "native"-type configuration directories.  "IV"
1241
+        means "initialization vector", and "MAC" means "message
1242
+        authentication code".  They are specifically cryptographic
1243
+        terms, as is "payload".  The acronyms "IV" and "MAC" are assumed
1244
+        to be well-known to the English target audience, or at least
1245
+        discoverable by them; they *are* asking for debug output, after
1246
+        all.  Please use your judgement as to whether to translate this
1247
+        term or not, expanded or not.
1248
+        """,
1249
+        msg='Parsing IV, payload and MAC from the file contents',
1250
+        context='info message',
1251
+    )
818 1252
 
819 1253
 
820 1254
 class WarnMsgTemplate(enum.Enum):
... ...
@@ -1250,11 +1684,12 @@ def _write_pot_file(fileobj: TextIO) -> None:  # pragma: no cover
1250 1684
         str,
1251 1685
         dict[
1252 1686
             str,
1253
-            Label | InfoMsgTemplate | WarnMsgTemplate | ErrMsgTemplate,
1687
+            Label | DebugMsgTemplate | InfoMsgTemplate | WarnMsgTemplate | ErrMsgTemplate,
1254 1688
         ],
1255 1689
     ] = {}
1256 1690
     for enum_class in (
1257 1691
         Label,
1692
+        DebugMsgTemplate,
1258 1693
         InfoMsgTemplate,
1259 1694
         WarnMsgTemplate,
1260 1695
         ErrMsgTemplate,
... ...
@@ -1304,7 +1739,7 @@ def _write_pot_file(fileobj: TextIO) -> None:  # pragma: no cover
1304 1739
 
1305 1740
 
1306 1741
 def _format_po_entry(
1307
-    enum_value: Label | InfoMsgTemplate | WarnMsgTemplate | ErrMsgTemplate,
1742
+    enum_value: Label | DebugMsgTemplate | InfoMsgTemplate | WarnMsgTemplate | ErrMsgTemplate,
1308 1743
 ) -> tuple[str, ...]:  # pragma: no cover
1309 1744
     ret: list[str] = ['\n']
1310 1745
     ts = enum_value.value
... ...
@@ -31,6 +31,7 @@ import os.path
31 31
 import struct
32 32
 from typing import TYPE_CHECKING, Any, TypedDict
33 33
 
34
+from derivepassphrase import _cli_msg as _msg
34 35
 from derivepassphrase import exporter
35 36
 
36 37
 if TYPE_CHECKING:
... ...
@@ -39,6 +40,7 @@ if TYPE_CHECKING:
39 40
     from cryptography.hazmat.primitives import ciphers, hashes, hmac, padding
40 41
     from cryptography.hazmat.primitives.ciphers import algorithms, modes
41 42
     from cryptography.hazmat.primitives.kdf import pbkdf2
43
+    from typing_extensions import Buffer
42 44
 else:
43 45
     try:
44 46
         from cryptography.hazmat.primitives import (
... ...
@@ -79,6 +81,10 @@ __all__ = ('export_storeroom_data',)
79 81
 logger = logging.getLogger(__name__)
80 82
 
81 83
 
84
+def _h(bs: Buffer) -> str:
85
+    return '<{}>'.format(memoryview(bs).hex(' '))
86
+
87
+
82 88
 class KeyPair(TypedDict):
83 89
     """A pair of AES256 keys, one for encryption and one for signing.
84 90
 
... ...
@@ -160,20 +166,16 @@ def derive_master_keys_keys(password: str | bytes, iterations: int) -> KeyPair:
160 166
         f'{KEY_SIZE}s {KEY_SIZE}s', master_keys_keys_blob
161 167
     )
162 168
     logger.debug(
163
-        (
164
-            'derived master_keys_keys bytes.fromhex(%s) (encryption) '
165
-            'and bytes.fromhex(%s) (signing) '
166
-            'from password bytes.fromhex(%s), '
167
-            'using call '
168
-            'pbkdf2(algorithm=%s, length=%d, salt=%s, iterations=%d)'
169
+        _msg.TranslatedString(
170
+            _msg.DebugMsgTemplate.DERIVED_MASTER_KEYS_KEYS,
171
+            enc_key=_h(encryption_key),
172
+            sign_key=_h(signing_key),
173
+            pw_bytes=_h(password),
174
+            algorithm='SHA256',
175
+            length=64,
176
+            salt=STOREROOM_MASTER_KEYS_UUID,
177
+            iterations=iterations,
169 178
         ),
170
-        repr(encryption_key.hex(' ')),
171
-        repr(signing_key.hex(' ')),
172
-        repr(password.hex(' ')),
173
-        repr('SHA256'),
174
-        64,
175
-        repr(STOREROOM_MASTER_KEYS_UUID),
176
-        iterations,
177 179
     )
178 180
     return {
179 181
         'encryption_key': encryption_key,
... ...
@@ -234,16 +236,13 @@ def decrypt_master_keys_data(data: bytes, keys: KeyPair) -> MasterKeys:
234 236
     actual_mac = hmac.HMAC(keys['signing_key'], hashes.SHA256())
235 237
     actual_mac.update(ciphertext)
236 238
     logger.debug(
237
-        (
238
-            'master_keys_data mac_key = bytes.fromhex(%s), '
239
-            'hashed_content = bytes.fromhex(%s), '
240
-            'claimed_mac = bytes.fromhex(%s), '
241
-            'actual_mac = bytes.fromhex(%s)'
239
+        _msg.TranslatedString(
240
+            _msg.DebugMsgTemplate.MASTER_KEYS_DATA_MAC_INFO,
241
+            sign_key=_h(keys['signing_key']),
242
+            ciphertext=_h(ciphertext),
243
+            claimed_mac=_h(claimed_mac),
244
+            actual_mac=_h(actual_mac.copy().finalize()),
242 245
         ),
243
-        repr(keys['signing_key'].hex(' ')),
244
-        repr(ciphertext.hex(' ')),
245
-        repr(claimed_mac.hex(' ')),
246
-        repr(actual_mac.copy().finalize().hex(' ')),
247 246
     )
248 247
     actual_mac.verify(claimed_mac)
249 248
 
... ...
@@ -325,17 +324,13 @@ def decrypt_session_keys(data: bytes, master_keys: MasterKeys) -> KeyPair:
325 324
     actual_mac = hmac.HMAC(master_keys['signing_key'], hashes.SHA256())
326 325
     actual_mac.update(ciphertext)
327 326
     logger.debug(
328
-        (
329
-            'decrypt_bucket_item (session_keys): '
330
-            'mac_key = bytes.fromhex(%s) (master), '
331
-            'hashed_content = bytes.fromhex(%s), '
332
-            'claimed_mac = bytes.fromhex(%s), '
333
-            'actual_mac = bytes.fromhex(%s)'
327
+        _msg.TranslatedString(
328
+            _msg.DebugMsgTemplate.DECRYPT_BUCKET_ITEM_SESSION_KEYS_MAC_INFO,
329
+            sign_key=_h(master_keys['signing_key']),
330
+            ciphertext=_h(ciphertext),
331
+            claimed_mac=_h(claimed_mac),
332
+            actual_mac=_h(actual_mac.copy().finalize()),
334 333
         ),
335
-        repr(master_keys['signing_key'].hex(' ')),
336
-        repr(ciphertext.hex(' ')),
337
-        repr(claimed_mac.hex(' ')),
338
-        repr(actual_mac.copy().finalize().hex(' ')),
339 334
     )
340 335
     actual_mac.verify(claimed_mac)
341 336
 
... ...
@@ -366,20 +361,19 @@ def decrypt_session_keys(data: bytes, master_keys: MasterKeys) -> KeyPair:
366 361
     }
367 362
 
368 363
     logger.debug(
369
-        (
370
-            'decrypt_bucket_item (session_keys): '
371
-            'decrypt_aes256_cbc_and_unpad(key=bytes.fromhex(%s), '
372
-            'iv=bytes.fromhex(%s))(bytes.fromhex(%s)) '
373
-            '= bytes.fromhex(%s) '
374
-            '= {"encryption_key": bytes.fromhex(%s), '
375
-            '"signing_key": bytes.fromhex(%s)}'
364
+        _msg.TranslatedString(
365
+            _msg.DebugMsgTemplate.DECRYPT_BUCKET_ITEM_SESSION_KEYS_INFO,
366
+            enc_key=_h(master_keys['encryption_key']),
367
+            iv=_h(iv),
368
+            ciphertext=_h(payload),
369
+            plaintext=_h(plaintext),
370
+            code=_msg.TranslatedString(
371
+                '{{"encryption_key": bytes.fromhex({enc_key!r}), '
372
+                '"signing_key": bytes.fromhex({sign_key!r})}}',
373
+                enc_key=session_keys['encryption_key'].hex(' '),
374
+                sign_key=session_keys['signing_key'].hex(' '),
375
+            ),
376 376
         ),
377
-        repr(master_keys['encryption_key'].hex(' ')),
378
-        repr(iv.hex(' ')),
379
-        repr(payload.hex(' ')),
380
-        repr(plaintext.hex(' ')),
381
-        repr(session_keys['encryption_key'].hex(' ')),
382
-        repr(session_keys['signing_key'].hex(' ')),
383 377
     )
384 378
 
385 379
     return session_keys
... ...
@@ -432,17 +426,13 @@ def decrypt_contents(data: bytes, session_keys: KeyPair) -> bytes:
432 426
     actual_mac = hmac.HMAC(session_keys['signing_key'], hashes.SHA256())
433 427
     actual_mac.update(ciphertext)
434 428
     logger.debug(
435
-        (
436
-            'decrypt_bucket_item (contents): '
437
-            'mac_key = bytes.fromhex(%s), '
438
-            'hashed_content = bytes.fromhex(%s), '
439
-            'claimed_mac = bytes.fromhex(%s), '
440
-            'actual_mac = bytes.fromhex(%s)'
429
+        _msg.TranslatedString(
430
+            _msg.DebugMsgTemplate.DECRYPT_BUCKET_ITEM_MAC_INFO,
431
+            sign_key=_h(session_keys['signing_key']),
432
+            ciphertext=_h(ciphertext),
433
+            claimed_mac=_h(claimed_mac),
434
+            actual_mac=_h(actual_mac.copy().finalize()),
441 435
         ),
442
-        repr(session_keys['signing_key'].hex(' ')),
443
-        repr(ciphertext.hex(' ')),
444
-        repr(claimed_mac.hex(' ')),
445
-        repr(actual_mac.copy().finalize().hex(' ')),
446 436
     )
447 437
     actual_mac.verify(claimed_mac)
448 438
 
... ...
@@ -461,16 +451,13 @@ def decrypt_contents(data: bytes, session_keys: KeyPair) -> bytes:
461 451
     plaintext.extend(unpadder.finalize())
462 452
 
463 453
     logger.debug(
464
-        (
465
-            'decrypt_bucket_item (contents): '
466
-            'decrypt_aes256_cbc_and_unpad(key=bytes.fromhex(%s), '
467
-            'iv=bytes.fromhex(%s))(bytes.fromhex(%s)) '
468
-            '= bytes.fromhex(%s)'
454
+        _msg.TranslatedString(
455
+            _msg.DebugMsgTemplate.DECRYPT_BUCKET_ITEM_INFO,
456
+            enc_key=_h(session_keys['encryption_key']),
457
+            iv=_h(iv),
458
+            ciphertext=_h(payload),
459
+            plaintext=_h(plaintext),
469 460
         ),
470
-        repr(session_keys['encryption_key'].hex(' ')),
471
-        repr(iv.hex(' ')),
472
-        repr(payload.hex(' ')),
473
-        repr(plaintext.hex(' ')),
474 461
     )
475 462
 
476 463
     return plaintext
... ...
@@ -505,14 +492,12 @@ def decrypt_bucket_item(bucket_item: bytes, master_keys: MasterKeys) -> bytes:
505 492
 
506 493
     """
507 494
     logger.debug(
508
-        (
509
-            'decrypt_bucket_item: data = bytes.fromhex(%s), '
510
-            'encryption_key = bytes.fromhex(%s), '
511
-            'signing_key = bytes.fromhex(%s)'
495
+        _msg.TranslatedString(
496
+            _msg.DebugMsgTemplate.DECRYPT_BUCKET_ITEM_KEY_INFO,
497
+            plaintext=_h(bucket_item),
498
+            enc_key=_h(master_keys['encryption_key']),
499
+            sign_key=_h(master_keys['signing_key']),
512 500
         ),
513
-        repr(bucket_item.hex(' ')),
514
-        repr(master_keys['encryption_key'].hex(' ')),
515
-        repr(master_keys['signing_key'].hex(' ')),
516 501
     )
517 502
     data_version, encrypted_session_keys, data_contents = struct.unpack(
518 503
         (
... ...
@@ -665,7 +650,9 @@ def export_storeroom_data(  # noqa: C901,PLR0912,PLR0914,PLR0915
665 650
     if encrypted_keys_version != 1:
666 651
         msg = f'cannot handle version {encrypted_keys_version} encrypted keys'
667 652
         raise RuntimeError(msg)
668
-    logger.info('Parsing master keys data from .keys')
653
+    logger.info(
654
+        _msg.TranslatedString(_msg.InfoMsgTemplate.PARSING_MASTER_KEYS_DATA)
655
+    )
669 656
     encrypted_keys_iterations = 2 ** (10 + (encrypted_keys_params & 0x0F))
670 657
     master_keys_keys = derive_master_keys_keys(
671 658
         master_keys_key, encrypted_keys_iterations
... ...
@@ -683,7 +670,12 @@ def export_storeroom_data(  # noqa: C901,PLR0912,PLR0914,PLR0915
683 670
         if fnmatch.fnmatch(hashdir_name, '[01][0-9a-f]')
684 671
     ]
685 672
     for file in valid_hashdirs:
686
-        logger.info('Decrypting bucket %s', file)
673
+        logger.info(
674
+            _msg.TranslatedString(
675
+                _msg.InfoMsgTemplate.DECRYPTING_BUCKET,
676
+                bucket_number=file,
677
+            )
678
+        )
687 679
         bucket_contents = list(
688 680
             decrypt_bucket_file(file, master_keys, root_dir=storeroom_path)
689 681
         )
... ...
@@ -691,17 +683,25 @@ def export_storeroom_data(  # noqa: C901,PLR0912,PLR0914,PLR0915
691 683
         for pos, item in enumerate(bucket_index):
692 684
             json_contents[item] = bucket_contents[pos]
693 685
             logger.debug(
694
-                'Found bucket item: %s -> %s', item, bucket_contents[pos]
686
+                _msg.TranslatedString(
687
+                    _msg.DebugMsgTemplate.BUCKET_ITEM_FOUND,
688
+                    path=item,
689
+                    value=bucket_contents[pos],
690
+                )
695 691
             )
696 692
     dirs_to_check: dict[str, list[str]] = {}
697 693
     json_payload: Any
698
-    logger.info('Assembling config structure')
694
+    logger.info(
695
+        _msg.TranslatedString(_msg.InfoMsgTemplate.ASSEMBLING_CONFIG_STRUCTURE)
696
+    )
699 697
     for path, json_content in sorted(json_contents.items()):
700 698
         if path.endswith('/'):
701 699
             logger.debug(
702
-                'Postponing dir check: %s -> %s',
703
-                path,
704
-                json_content.decode('utf-8'),
700
+                _msg.TranslatedString(
701
+                    _msg.DebugMsgTemplate.POSTPONING_DIRECTORY_CONTENTS_CHECK,
702
+                    path=path,
703
+                    contents=json_content.decode('utf-8'),
704
+                )
705 705
             )
706 706
             json_payload = json.loads(json_content)
707 707
             if not isinstance(json_payload, list) or any(
... ...
@@ -714,17 +714,26 @@ def export_storeroom_data(  # noqa: C901,PLR0912,PLR0914,PLR0915
714 714
                 raise RuntimeError(msg)
715 715
             dirs_to_check[path] = json_payload
716 716
             logger.debug(
717
-                'Setting contents (empty directory): %s -> %s', path, '{}'
717
+                _msg.TranslatedString(
718
+                    _msg.DebugMsgTemplate.SETTING_CONFIG_STRUCTURE_CONTENTS_EMPTY_DIRECTORY,
719
+                    path=path,
720
+                ),
718 721
             )
719 722
             _store(config_structure, path, b'{}')
720 723
         else:
721 724
             logger.debug(
722
-                'Setting contents: %s -> %s',
723
-                path,
724
-                json_content.decode('utf-8'),
725
+                _msg.TranslatedString(
726
+                    _msg.DebugMsgTemplate.SETTING_CONFIG_STRUCTURE_CONTENTS,
727
+                    path=path,
728
+                    value=json_content.decode('utf-8'),
729
+                ),
725 730
             )
726 731
             _store(config_structure, path, json_content)
727
-    logger.info('Checking structure consistency')
732
+    logger.info(
733
+        _msg.TranslatedString(
734
+            _msg.InfoMsgTemplate.CHECKING_CONFIG_STRUCTURE_CONSISTENCY,
735
+        )
736
+    )
728 737
     # Sorted order is important; see `maybe_obj` below.
729 738
     for _dir, namelist in sorted(dirs_to_check.items()):
730 739
         namelist = [x.rstrip('/') for x in namelist]  # noqa: PLW2901
... ...
@@ -746,6 +755,13 @@ def export_storeroom_data(  # noqa: C901,PLR0912,PLR0914,PLR0915
746 755
         if set(obj.keys()) != set(namelist):
747 756
             msg = f'Object key mismatch for path {_dir!r}'
748 757
             raise RuntimeError(msg)
758
+        logger.debug(
759
+            _msg.TranslatedString(
760
+                _msg.DebugMsgTemplate.DIRECTORY_CONTENTS_CHECK_OK,
761
+                path=_dir,
762
+                contents=json.dumps(namelist),
763
+            )
764
+        )
749 765
     return config_structure
750 766
 
751 767
 
... ...
@@ -30,6 +30,7 @@ import logging
30 30
 import warnings
31 31
 from typing import TYPE_CHECKING
32 32
 
33
+from derivepassphrase import _cli_msg as _msg
33 34
 from derivepassphrase import exporter, vault
34 35
 
35 36
 if TYPE_CHECKING:
... ...
@@ -80,8 +81,8 @@ __all__ = ('export_vault_native_data',)
80 81
 logger = logging.getLogger(__name__)
81 82
 
82 83
 
83
-def _h(bs: bytes | bytearray) -> str:
84
-    return 'bytes.fromhex({!r})'.format(bs.hex(' '))
84
+def _h(bs: Buffer) -> str:
85
+    return '<{}>'.format(memoryview(bs).hex(' '))
85 86
 
86 87
 
87 88
 class VaultNativeConfigParser(abc.ABC):
... ...
@@ -174,19 +175,25 @@ class VaultNativeConfigParser(abc.ABC):
174 175
         ).derive(bytes(password))
175 176
         result_key = raw_key.hex().lower().encode('ASCII')
176 177
         logger.debug(
177
-            'binary = pbkdf2(%s, %s, %s, %s, %s) = %s -> %s',
178
-            repr(password),
179
-            repr(vault.Vault._UUID),  # noqa: SLF001
180
-            iterations,
181
-            key_size // 2,
182
-            repr('sha1'),
183
-            _h(raw_key),
184
-            _h(result_key),
178
+            _msg.TranslatedString(
179
+                _msg.DebugMsgTemplate.VAULT_NATIVE_PBKDF2_CALL,
180
+                password=password,
181
+                salt=vault.Vault._UUID,  # noqa: SLF001
182
+                iterations=iterations,
183
+                key_size=key_size // 2,
184
+                algorithm='sha1',
185
+                raw_result=raw_key,
186
+                result_key=result_key.decode('ASCII'),
187
+            ),
185 188
         )
186 189
         return result_key
187 190
 
188 191
     def _parse_contents(self) -> None:
189
-        logger.info('Parsing IV, payload and signature from the file contents')
192
+        logger.info(
193
+            _msg.TranslatedString(
194
+                _msg.InfoMsgTemplate.VAULT_NATIVE_PARSING_IV_PAYLOAD_MAC,
195
+            ),
196
+        )
190 197
 
191 198
         if len(self._contents) < self._iv_size + 16 + self._mac_size:
192 199
             msg = 'Invalid vault configuration file: file is truncated'
... ...
@@ -202,15 +209,21 @@ class VaultNativeConfigParser(abc.ABC):
202 209
         self._iv, self._payload = cut(self._message, cutpos2)
203 210
 
204 211
         logger.debug(
205
-            'buffer %s = [[%s, %s], %s]',
206
-            _h(self._contents),
207
-            _h(self._iv),
208
-            _h(self._payload),
209
-            _h(self._message_tag),
212
+            _msg.TranslatedString(
213
+                _msg.DebugMsgTemplate.VAULT_NATIVE_PARSE_BUFFER,
214
+                contents=_h(self._contents),
215
+                iv=_h(self._iv),
216
+                payload=_h(self._payload),
217
+                mac=_h(self._message_tag),
218
+            ),
210 219
         )
211 220
 
212 221
     def _derive_keys(self) -> None:
213
-        logger.info('Deriving an encryption and signing key')
222
+        logger.info(
223
+            _msg.TranslatedString(
224
+                _msg.InfoMsgTemplate.VAULT_NATIVE_DERIVING_KEYS,
225
+            ),
226
+        )
214 227
         self._generate_keys()
215 228
         assert len(self._encryption_key) == self._encryption_key_size, (
216 229
             'Derived encryption key is invalid'
... ...
@@ -224,13 +237,19 @@ class VaultNativeConfigParser(abc.ABC):
224 237
         raise AssertionError
225 238
 
226 239
     def _check_signature(self) -> None:
227
-        logger.info('Checking HMAC signature')
240
+        logger.info(
241
+            _msg.TranslatedString(
242
+                _msg.InfoMsgTemplate.VAULT_NATIVE_CHECKING_MAC,
243
+            ),
244
+        )
228 245
         mac = hmac.HMAC(self._signing_key, hashes.SHA256())
229 246
         mac_input = self._hmac_input()
230 247
         logger.debug(
231
-            'mac_input = %s, expected_tag = %s',
232
-            _h(mac_input),
233
-            _h(self._message_tag),
248
+            _msg.TranslatedString(
249
+                _msg.DebugMsgTemplate.VAULT_NATIVE_CHECKING_MAC_DETAILS,
250
+                mac_input=_h(mac_input),
251
+                mac=_h(self._message_tag),
252
+            ),
234 253
         )
235 254
         mac.update(mac_input)
236 255
         try:
... ...
@@ -244,16 +263,31 @@ class VaultNativeConfigParser(abc.ABC):
244 263
         raise AssertionError
245 264
 
246 265
     def _decrypt_payload(self) -> Any:  # noqa: ANN401
266
+        logger.info(
267
+            _msg.TranslatedString(
268
+                _msg.InfoMsgTemplate.VAULT_NATIVE_DECRYPTING_CONTENTS,
269
+            ),
270
+        )
247 271
         decryptor = self._make_decryptor()
248 272
         padded_plaintext = bytearray()
249 273
         padded_plaintext.extend(decryptor.update(self._payload))
250 274
         padded_plaintext.extend(decryptor.finalize())
251
-        logger.debug('padded plaintext = %s', _h(padded_plaintext))
275
+        logger.debug(
276
+            _msg.TranslatedString(
277
+                _msg.DebugMsgTemplate.VAULT_NATIVE_PADDED_PLAINTEXT,
278
+                contents=_h(padded_plaintext),
279
+            ),
280
+        )
252 281
         unpadder = padding.PKCS7(self._iv_size * 8).unpadder()
253 282
         plaintext = bytearray()
254 283
         plaintext.extend(unpadder.update(padded_plaintext))
255 284
         plaintext.extend(unpadder.finalize())
256
-        logger.debug('plaintext = %s', _h(plaintext))
285
+        logger.debug(
286
+            _msg.TranslatedString(
287
+                _msg.DebugMsgTemplate.VAULT_NATIVE_PLAINTEXT,
288
+                contents=_h(plaintext),
289
+            ),
290
+        )
257 291
         return json.loads(plaintext)
258 292
 
259 293
     @abc.abstractmethod
... ...
@@ -280,12 +314,6 @@ class VaultNativeV03ConfigParser(VaultNativeConfigParser):
280 314
         self._iv_size = 16
281 315
         self._mac_size = 32
282 316
 
283
-    def __call__(self) -> Any:  # noqa: ANN401
284
-        if self._data is self._sentinel:
285
-            logger.info('Attempting to parse as v0.3 configuration')
286
-            return super().__call__()
287
-        return self._data
288
-
289 317
     def _generate_keys(self) -> None:
290 318
         self._encryption_key = self._pbkdf2(self._password, self.KEY_SIZE, 100)
291 319
         self._signing_key = self._pbkdf2(self._password, self.KEY_SIZE, 200)
... ...
@@ -323,17 +351,17 @@ class VaultNativeV02ConfigParser(VaultNativeConfigParser):
323 351
         self._iv_size = 16
324 352
         self._mac_size = 64
325 353
 
326
-    def __call__(self) -> Any:  # noqa: ANN401
327
-        if self._data is self._sentinel:
328
-            logger.info('Attempting to parse as v0.2 configuration')
329
-            return super().__call__()
330
-        return self._data
331
-
332 354
     def _parse_contents(self) -> None:
333 355
         super()._parse_contents()
334
-        logger.debug('Decoding payload (base64) and message tag (hex)')
335 356
         self._payload = base64.standard_b64decode(self._payload)
336 357
         self._message_tag = bytes.fromhex(self._message_tag.decode('ASCII'))
358
+        logger.debug(
359
+            _msg.TranslatedString(
360
+                _msg.DebugMsgTemplate.VAULT_NATIVE_V02_PAYLOAD_MAC_POSTPROCESSING,
361
+                payload=_h(self._payload),
362
+                mac=_h(self._message_tag),
363
+            ),
364
+        )
337 365
 
338 366
     def _generate_keys(self) -> None:
339 367
         self._encryption_key = self._pbkdf2(self._password, 8, 16)
... ...
@@ -353,16 +381,15 @@ class VaultNativeV02ConfigParser(VaultNativeConfigParser):
353 381
             last_block = b''
354 382
             salt = b''
355 383
             logger.debug(
356
-                (
357
-                    'data = %s, salt = %s, key_size = %s, iv_size = %s, '
358
-                    'buffer length = %s, buffer = %s'
384
+                _msg.TranslatedString(
385
+                    _msg.DebugMsgTemplate.VAULT_NATIVE_EVP_BYTESTOKEY_INIT,
386
+                    data=_h(data),
387
+                    salt=_h(salt),
388
+                    key_size=key_size,
389
+                    iv_size=iv_size,
390
+                    buffer_length=len(buffer),
391
+                    buffer=_h(buffer),
359 392
                 ),
360
-                _h(data),
361
-                _h(salt),
362
-                key_size,
363
-                iv_size,
364
-                len(buffer),
365
-                _h(buffer),
366 393
             )
367 394
             while len(buffer) < total_size:
368 395
                 with warnings.catch_warnings():
... ...
@@ -376,12 +403,18 @@ class VaultNativeV02ConfigParser(VaultNativeConfigParser):
376 403
                 last_block = block.finalize()
377 404
                 buffer.extend(last_block)
378 405
                 logger.debug(
379
-                    'buffer length = %s, buffer = %s', len(buffer), _h(buffer)
406
+                    _msg.TranslatedString(
407
+                        _msg.DebugMsgTemplate.VAULT_NATIVE_EVP_BYTESTOKEY_ROUND,
408
+                        buffer_length=len(buffer),
409
+                        buffer=_h(buffer),
410
+                    ),
380 411
                 )
381 412
             logger.debug(
382
-                'encryption_key = %s, iv = %s',
383
-                _h(buffer[:key_size]),
384
-                _h(buffer[key_size:total_size]),
413
+                _msg.TranslatedString(
414
+                    _msg.DebugMsgTemplate.VAULT_NATIVE_EVP_BYTESTOKEY_RESULT,
415
+                    enc_key=_h(buffer[:key_size]),
416
+                    iv=_h(buffer[key_size:total_size]),
417
+                ),
385 418
             )
386 419
             return bytes(buffer[:key_size]), bytes(buffer[key_size:total_size])
387 420
 
388 421