ansible-vars 1.0.7__py3-none-any.whl → 1.0.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ansible_vars/cli.py CHANGED
@@ -84,6 +84,10 @@ Specify vault keys to load for en-/decryption. Not required for vars files with
84
84
  A key is a combination of an identifier (can be a vault ID or anything else, ideally unique) and a passphrase.
85
85
  By default, available keys are auto-detected if your current directory contains an Ansible config and appended to the ones you supplied.
86
86
  If no explicit encryption key is specified, the first supplied/available key is used.
87
+
88
+ [!] When editing a hybrid vault file, changing even a single variable will change all variables' ciphers, as salts are generated randomly.
89
+ You can prevent this by passing a fixed salt via `-S <salt>`, which will result in any plaintext always resolving to the same cipher.
90
+ For Ansible's AES-256, a length of at least 32 characters is recommended.
87
91
  ''',
88
92
  'log_args': '''
89
93
  Log a diff of any vault changes performed with this program to an encrypted or plain logfile, creating it if necessary.
@@ -130,6 +134,10 @@ Decrypt a string and return it or fully decrypt a file in-place. Uses the first
130
134
  ''',
131
135
  'cmd_is_enc': '''
132
136
  Check if a string or file is (fully) vault-encrypted.
137
+ ''',
138
+ 'cmd_rekey': '''
139
+ Update a vault's ciphers with a new encryption key and/or salt.
140
+ The key referenced by `--encryption-key|-K <identifier>` and/or the salt set by `--fixed-salt|-S <salt>` are used for re-encryption.
133
141
  ''',
134
142
  'cmd_convert': '''
135
143
  Switch a file between full outer and full inner encryption for convenient migrating between encryption schemes.
@@ -198,6 +206,7 @@ DEFAULT_EDITOR: str = os.environ.get('EDITOR', 'notepad.exe' if os.name == 'nt'
198
206
  DEFAULT_COLOR_MODE: str = os.environ.get('AV_COLOR_MODE', '256' if sys.stdout.isatty() else 'none')
199
207
  DEFAULT_TEMP_DIR: str = os.environ.get('AV_TEMP_DIR', gettempdir())
200
208
  DEFAULT_CREATE_PLAIN: bool = os.environ.get('AV_CREATE_PLAIN', 'no').lower() in [ 'yes', 'y', 'true', 't', '1' ]
209
+ DEFAULT_SALT: str | None = os.environ.get('AV_SALT', None)
201
210
 
202
211
  args: ArgumentParser = ArgumentParser(
203
212
  prog = 'ansible-vars',
@@ -243,6 +252,10 @@ key_args.add_argument(
243
252
  )
244
253
  key_args.add_argument('--no-detect-keys', '-D', action='store_false', dest='detect_keys', help='disable automatic key detection')
245
254
  key_args.add_argument('--encryption-key', '-K', type=str, metavar='<identifier>', help='which of the loaded keys to use for encryption')
255
+ key_args.add_argument(
256
+ '--fixed-salt', '-S', type=str, metavar='<salt>', default=DEFAULT_SALT,
257
+ help='a fixed salt to use for encryption (should be 32+ chars!)'
258
+ )
246
259
 
247
260
  log_args = args.add_argument_group('logging vault changes', description=HELP['log_args'])
248
261
  log_mutex = log_args.add_mutually_exclusive_group()
@@ -329,6 +342,13 @@ cmd_is_enc.add_argument('target', type=str, metavar='<vault path | string>', hel
329
342
  .completer = _prefixed_path_completer # type: ignore
330
343
  cmd_is_enc.add_argument('--quiet', '-q', action='store_true', help='no output, only set the rc to 0 if encrypted or 100 if unencrypted')
331
344
 
345
+ cmd_rekey = commands.add_parser(
346
+ 'rekey', help='update a vault\'s encryption key (from -K) and/or salt (from -S)', description=HELP['cmd_rekey'],
347
+ formatter_class=RawDescriptionHelpFormatter
348
+ )
349
+ cmd_rekey.add_argument('vault_path', type=str, metavar='<vault path>', help='path of vault to rekey') \
350
+ .completer = _prefixed_path_completer # type: ignore
351
+
332
352
  cmd_convert = commands.add_parser(
333
353
  'convert', help='switch vault between outer (file) and inner (vars) encryption', description=HELP['cmd_convert'],
334
354
  formatter_class=RawDescriptionHelpFormatter
@@ -565,7 +585,7 @@ sys.excepthook = _exc_hook
565
585
  # Load vault keys
566
586
 
567
587
  _explicit_keys: list[VaultKey] = [ VaultKey(passphrase, vault_id=id) for id, passphrase in config.keys ]
568
- keyring = VaultKeyring(_explicit_keys.copy(), detect_available_keys=config.detect_keys)
588
+ keyring = VaultKeyring(_explicit_keys.copy(), detect_available_keys=config.detect_keys, default_salt=config.fixed_salt)
569
589
 
570
590
  if config.encryption_key:
571
591
  keyring.default_encryption_key = keyring.key_by_id(config.encryption_key)
@@ -575,6 +595,8 @@ try:
575
595
  debug(f"Encryption key: { keyring.encryption_key.id }")
576
596
  except:
577
597
  debug('Encryption key: unavailable')
598
+ if config.fixed_salt:
599
+ debug(f"Using fixed encryption salt: { config.fixed_salt }")
578
600
 
579
601
  # Set up logging
580
602
 
@@ -690,7 +712,6 @@ if config.command in [ 'create', 'edit' ]:
690
712
  def _find_new_plain_vars(path: tuple[Hashable, ...], value: Any) -> Any:
691
713
  if path != ( SENTINEL_KEY, ) and type(value) is not EncryptedVar:
692
714
  if (old_value := vault.get(path, default=Unset)) != value:
693
- std_print(path[-1], type(old_value), type(value))
694
715
  if type(old_value) is EncryptedVar:
695
716
  decrypted_vars.append(path)
696
717
  else:
@@ -822,6 +843,21 @@ if config.command in [ 'encrypt', 'decrypt', 'is-encrypted' ]:
822
843
  else:
823
844
  print(f"Value is { 'encrypted' if is_encrypted else 'plain' }.", Color.GOOD if is_encrypted else Color.MEH)
824
845
 
846
+ # Rekey command
847
+
848
+ if config.command == 'rekey':
849
+ vault_path: str = resolve_vault_path(config.vault_path)
850
+ if not config.encryption_key:
851
+ print(f"No explicit encryption key specified, falling back to '{ keyring.encryption_key.id }'", Color.MEH)
852
+ # Since ciphers are usually not changed from load to save, we force re-encryption by loading from an editable
853
+ vault = VaultFile(vault_path, keyring=keyring)
854
+ vault = VaultFile.from_editable(vault, vault.as_editable())
855
+ vault.save()
856
+ print(
857
+ f"Re-encrypted vault with key '{ keyring.encryption_key.id }' and a { 'fixed' if config.fixed_salt else 'random' } salt",
858
+ Color.GOOD
859
+ )
860
+
825
861
  # Convert command
826
862
 
827
863
  if config.command == 'convert':
ansible_vars/util.py CHANGED
@@ -338,7 +338,7 @@ class VaultDaemon(FileSystemEventHandler):
338
338
  content: str = src.read()
339
339
  with open(self.target_file, 'w') as tgt:
340
340
  try:
341
- tgt.write(Vault(content, keyring=self.keyring).as_editable(with_header=False))
341
+ tgt.write(Vault(content, keyring=self.keyring).as_plain())
342
342
  self.debug(f"Created/Updated file { self.target_file } from vault contents")
343
343
  except:
344
344
  tgt.write(content)
@@ -373,7 +373,7 @@ class VaultDaemon(FileSystemEventHandler):
373
373
  content: str = src.read()
374
374
  with open(target_path, 'w') as tgt:
375
375
  try:
376
- tgt.write(Vault(content, keyring=self.keyring).as_editable(with_header=False))
376
+ tgt.write(Vault(content, keyring=self.keyring).as_plain())
377
377
  self.debug(f"Created/Updated file { target_path } from vault contents")
378
378
  except:
379
379
  tgt.write(content)
@@ -54,12 +54,12 @@ class VaultKey():
54
54
  '''
55
55
  return VaultLib.is_encrypted(VaultKey._strip_vault_tag(test_me))
56
56
 
57
- def encrypt(self, plain: str) -> str:
58
- '''Encrypts a string using this `VaultKey`'s secret.'''
57
+ def encrypt(self, plain: str, salt: str | None = None) -> str:
58
+ '''Encrypts a string using this `VaultKey`'s secret. You can specify an optional fixed salt (ideally 32+ chars).'''
59
59
  # Pass our secret directly to the encrypt call to skip expensive secret matching
60
60
  # Beware: the encrypt function takes a `secret`, but means just the VaultSecret and not a tuple of (vault_id, VaultSecret)
61
61
  # In other calls, `secret` or `secrets` may refer to the tuple(s)
62
- return self._vaultlib.encrypt(plain, secret=self.secret, vault_id=self.id).decode('utf-8').strip()
62
+ return self._vaultlib.encrypt(plain, secret=self.secret, vault_id=self.id, salt=salt).decode('utf-8').strip()
63
63
 
64
64
  def decrypt(self, vault_cipher: str) -> str:
65
65
  '''
@@ -96,6 +96,7 @@ class VaultKeyring():
96
96
  self,
97
97
  keys: list[VaultKey] | None = None,
98
98
  default_encryption_key: VaultKey | None = None,
99
+ default_salt: str | None = None,
99
100
  detect_available_keys: bool = True
100
101
  ) -> None:
101
102
  '''
@@ -107,9 +108,13 @@ class VaultKeyring():
107
108
 
108
109
  When encrypting data, you can specify an explicit `VaultKey` to use. If none is specified, `default_encryption_key` is used.
109
110
  If no explicit or default keys are available, the first key of the `keys` parameter is used.
111
+
112
+ You can also set a fixed salt to use for encryption tasks, either passing it to the `encrypt` function directly
113
+ or as a `default_salt` here. A length of at least 32 chars is recommended for Ansible's AES-256.
110
114
  '''
111
115
  self.keys: list[VaultKey] = keys or []
112
116
  self.default_encryption_key: VaultKey | None = default_encryption_key
117
+ self.default_salt: str | None = default_salt
113
118
  if detect_available_keys:
114
119
  self.keys.extend(VaultKeyring.load_cli_secrets())
115
120
 
@@ -124,16 +129,15 @@ class VaultKeyring():
124
129
  raise NoVaultKeysError('No vault keys available for encryption')
125
130
  return self.default_encryption_key or self.keys[0]
126
131
 
127
- def encrypt(self, plain: str, key: VaultKey | None = None) -> str:
132
+ def encrypt(self, plain: str, key: VaultKey | None = None, salt: str | None = None) -> str:
128
133
  '''
129
134
  Encrypts the given vault data using the supplied `VaultKey`.
130
135
  If no key is supplied, the `VaultKeyring`'s `default_encryption_key` is used.
131
136
  If that key is also unset, the first key of the `VaultKeyring`'s `keys` is used.
137
+ You can specify an optional fixed salt for encrypting (ideally 32+ chars).
132
138
  '''
133
139
  # If no key is provided, use the default encryption key or first key in `keys`
134
- if not key:
135
- key = self.encryption_key
136
- return key.encrypt(plain)
140
+ return (key or self.encryption_key).encrypt(plain, salt=(salt or self.default_salt))
137
141
 
138
142
  def decrypt(self, vault_cipher: str, key: VaultKey | None = None) -> str:
139
143
  '''
@@ -143,7 +147,7 @@ class VaultKeyring():
143
147
 
144
148
  Expects a cipher with optional YAML tag preamble (`!vault | $ANSIBLE_VAULT;<options>\\n<cipher>`).
145
149
  '''
146
- if not key and not self.keys:
150
+ if not (key or self.keys):
147
151
  raise NoVaultKeysError('No vault keys available for decryption')
148
152
  if key:
149
153
  return key.decrypt(vault_cipher)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ansible-vars
3
- Version: 1.0.7
3
+ Version: 1.0.9
4
4
  Summary: Manage vaults and variable files for Ansible
5
5
  Project-URL: Homepage, https://github.com/xorwow/ansible-vars
6
6
  Project-URL: Issues, https://github.com/xorwow/ansible-vars/issues
@@ -105,6 +105,10 @@ By default, the first loaded key is used for all encryption tasks. Note that aut
105
105
 
106
106
  You can disable automatic key detection by flagging `--no-encrypt-keys|-D`. Use `ansible-vars keyring` to view all available keys.
107
107
 
108
+ #### Encryption salts
109
+
110
+ Each time you edit a vault or otherwise encrypt a value, a randomly generated salt is used so even identical plain values will result in unique ciphers. However, this also means that each time a single encrypted variable is edited, all other ciphers in the vault will change as well. You can avoid this by passing a fixed salt via `--fixed-salt|-S <salt>`. Note that it should be at least 32 characters long and sufficiently random for Ansible's AES-256 encryption, and that you won't benefit from unique ciphers for identical plaintexts anymore.
111
+
108
112
  ### Diff logging
109
113
 
110
114
  **TL;DR:** You can use `-l <log directory>` to log changes to edited vaults to a vault-encrypted log file.
@@ -174,6 +178,10 @@ Shows the amounts of encrypted and decrypted variables in a vault file. Supports
174
178
 
175
179
  En-/Decrypts or checks the encryption status of a file or string value. Note that only full file encryption is considered in file mode, a hybrid vault with individually encrypted variables will be counted as plain.
176
180
 
181
+ ### rekey
182
+
183
+ Re-encrypts a vault file with a different encryption key and/or salt. The key specified in the global `--encryption-key|-K <identifier>` flag is used for encryption, along with an optional fixed salt set via the global `--fixed-salt|-S <salt>` flag.
184
+
177
185
  #### convert
178
186
 
179
187
  Convenience function to convert between fully encrypted vaults and hybrid vaults. Useful if you wish to convert your "legacy" fully encrypted vaults to plain files with all string values individually encrypted. Works both ways.
@@ -220,6 +228,10 @@ Set the tempfile/staging root as you would with `-T <path>`.
220
228
 
221
229
  Invert the default creation mode for files: If unset or `no`, files are created with full encryption unless specified otherwise via the `--plain|-p` flag. This behavior mirrors that of `ansible-vault`. When set to `yes`, the behavior and flag are inverted as files are created without encryption by default unless specified otherwise via the `--no-plain|-P` flag.
222
230
 
231
+ #### AV_SALT
232
+
233
+ Set a fixed salt as you would with `-S <salt>`.
234
+
223
235
  ### Python library
224
236
 
225
237
  When using `ansible-vars` as a library, import any of these modules from the `ansible_vars` module.
@@ -270,7 +282,6 @@ When editing a file or creating a daemon, decrypted vaults are written to disk t
270
282
  - Changes to file metadata (permissions, ...) are not mirrored.
271
283
  - `ansible-vars` does not support files which are not (Jinja2) YAML dictionaries, except for limited support in these commands:
272
284
  - `edit`, `view` (without `--json` support), `encrypt`, `decrypt`, `is-encrypted`
273
- - When a vault is modified, all of its ciphers will change due to new salts, even if only one encrypted variable has been changed.
274
285
 
275
286
  ## Extension plans
276
287
 
@@ -0,0 +1,12 @@
1
+ ansible_vars/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ ansible_vars/cli.py,sha256=00fZYY2z-N5KaeBlNICGCy9i8FQy-m6ita5c3etN_sY,65213
3
+ ansible_vars/constants.py,sha256=Nd3sIuSoOvyfUfHfnsnJBDGMW7eNzbMm1NAvEQio9hE,1624
4
+ ansible_vars/errors.py,sha256=6dzyksPKWira9O2-Ir3MIOwr4XjN9MSBiRp5e6siY6Q,1256
5
+ ansible_vars/util.py,sha256=UwGPBT19pee7lBpWuBzLPAvcrHUBAn6i1MrJvzM9OQ4,21265
6
+ ansible_vars/vault.py,sha256=cMvFdtc3bw6yf-aChUEP34k2yafWS2UuubFO84De_rA,46383
7
+ ansible_vars/vault_crypt.py,sha256=nh2k686nTI3yERIp-qzx5iDE1kZKg10YG019QeZDnLM,10019
8
+ ansible_vars-1.0.9.dist-info/METADATA,sha256=KmVgXHswpqOPFA5AegqKBLqcv0FFCfIgzdkr6NESp4c,18229
9
+ ansible_vars-1.0.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
10
+ ansible_vars-1.0.9.dist-info/entry_points.txt,sha256=RrhkEH0MbfRzflguVrfYfthsFC5V2fkFnizUG3uHMtQ,55
11
+ ansible_vars-1.0.9.dist-info/licenses/LICENSE,sha256=ocyJHLG5wD12qB4uam2pqWTHIJmzloiyNyTex6Q2DKo,1062
12
+ ansible_vars-1.0.9.dist-info/RECORD,,
@@ -1,12 +0,0 @@
1
- ansible_vars/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- ansible_vars/cli.py,sha256=OAIXPLMTinS48e9Wn9YEhdHt6eaBHuU9Px1qZ5peCKY,63345
3
- ansible_vars/constants.py,sha256=Nd3sIuSoOvyfUfHfnsnJBDGMW7eNzbMm1NAvEQio9hE,1624
4
- ansible_vars/errors.py,sha256=6dzyksPKWira9O2-Ir3MIOwr4XjN9MSBiRp5e6siY6Q,1256
5
- ansible_vars/util.py,sha256=j6Ay_TXEyCm2OUBV7VRXlGjipKSAuklIHXSXIxbrzj0,21305
6
- ansible_vars/vault.py,sha256=cMvFdtc3bw6yf-aChUEP34k2yafWS2UuubFO84De_rA,46383
7
- ansible_vars/vault_crypt.py,sha256=Eotf1JVJcvrdae2gv2KMVor-x1D1IMREkONKgKdtT2A,9493
8
- ansible_vars-1.0.7.dist-info/METADATA,sha256=otjXXgLP1_XT-Xykt9_V8nyIi65aoRAKM2ukGxdu5pU,17464
9
- ansible_vars-1.0.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
10
- ansible_vars-1.0.7.dist-info/entry_points.txt,sha256=RrhkEH0MbfRzflguVrfYfthsFC5V2fkFnizUG3uHMtQ,55
11
- ansible_vars-1.0.7.dist-info/licenses/LICENSE,sha256=ocyJHLG5wD12qB4uam2pqWTHIJmzloiyNyTex6Q2DKo,1062
12
- ansible_vars-1.0.7.dist-info/RECORD,,