20
20
sys .exit ('Requires Python >= 3.9' )
21
21
22
22
import os , io , importlib .util , argparse , re , getpass , datetime , traceback , copy
23
- import jinja2 , jinja2 .sandbox , yaml , zoneinfo
23
+ import jinja2 , jinja2 .sandbox , yaml , zoneinfo , base64
24
24
25
25
from cryptography .hazmat .primitives import hashes
26
26
from cryptography .hazmat .primitives .kdf .pbkdf2 import PBKDF2HMAC
27
+ from cryptography .hazmat .primitives .kdf .scrypt import Scrypt
27
28
from cryptography .hazmat .primitives .hmac import HMAC
28
29
from cryptography .hazmat .primitives .padding import PKCS7
29
30
from cryptography .hazmat .primitives .ciphers import Cipher
30
31
from cryptography .hazmat .primitives .ciphers .algorithms import AES
31
32
from cryptography .hazmat .primitives .ciphers .modes import CTR
33
+ from cryptography .hazmat .primitives .ciphers .aead import ChaCha20Poly1305
34
+
32
35
from cryptography .exceptions import InvalidSignature
36
+ from cryptography .exceptions import InvalidTag
33
37
34
- __version__ = '1.23.0 '
38
+ __version__ = '1.23.1 '
35
39
36
- __all__ = ['JinjaFx' , 'Vault ' ]
40
+ __all__ = ['JinjaFx' , 'AnsibleVault' , 'Vaulty ' ]
37
41
38
42
def main ():
39
43
exc_source = None
@@ -105,7 +109,7 @@ def main():
105
109
raise Exception ('multiline stings not permitted' )
106
110
107
111
__get_vault_credentials (vpw , True )
108
- vtext = Vault ().encrypt (b_string , vpw [0 ])
112
+ vtext = AnsibleVault ().encrypt (b_string , vpw [0 ])
109
113
print ('!vault |\n ' + re .sub (r'^' , ' ' * 10 , vtext , flags = re .MULTILINE ))
110
114
111
115
else :
@@ -117,7 +121,7 @@ def main():
117
121
118
122
try :
119
123
with open (f , 'rb' ) as fh :
120
- vtext = Vault ().encrypt (fh .read (), vpw [0 ])
124
+ vtext = AnsibleVault ().encrypt (fh .read (), vpw [0 ])
121
125
122
126
with open (f , 'wb' ) as fh :
123
127
fh .write (vtext .encode ('utf-8' ))
@@ -137,7 +141,7 @@ def main():
137
141
if not args .decrypt :
138
142
b_vtext = sys .stdin .buffer .read ()
139
143
__get_vault_credentials (vpw )
140
- print (Vault ().decrypt (b_vtext , vpw [0 ]).decode ('utf-8' ))
144
+ print (AnsibleVault ().decrypt (b_vtext , vpw [0 ]).decode ('utf-8' ))
141
145
142
146
else :
143
147
__get_vault_credentials (vpw )
@@ -148,7 +152,7 @@ def main():
148
152
149
153
try :
150
154
with open (f , 'rb' ) as fh :
151
- plaintext = Vault ().decrypt (fh .read (), vpw [0 ])
155
+ plaintext = AnsibleVault ().decrypt (fh .read (), vpw [0 ])
152
156
153
157
with open (f , 'wb' ) as fh :
154
158
fh .write (plaintext )
@@ -387,7 +391,7 @@ def main():
387
391
def __decrypt_vault (vpw , string ):
388
392
if string .lstrip ().startswith ('$ANSIBLE_VAULT;' ):
389
393
__get_vault_credentials (vpw )
390
- return Vault ().decrypt (string .encode ('utf-8' ), vpw [0 ])
394
+ return AnsibleVault ().decrypt (string .encode ('utf-8' ), vpw [0 ])
391
395
return string .encode ('utf-8' )
392
396
393
397
@@ -1324,7 +1328,7 @@ def __jfx_now(self, fmt=None, tz='UTC'):
1324
1328
return str (datetime .datetime .now (tz = zoneinfo .ZoneInfo (tz )))
1325
1329
1326
1330
1327
- class Vault ():
1331
+ class AnsibleVault ():
1328
1332
def __derive_key (self , b_password , b_salt = None ):
1329
1333
if b_salt is None :
1330
1334
b_salt = os .urandom (32 )
@@ -1386,5 +1390,53 @@ def decrypt(self, b_string, password):
1386
1390
raise Exception ('data isn\' t ansible vault encrypted' )
1387
1391
1388
1392
1393
+ class Vaulty ():
1394
+ def __init__ (self ):
1395
+ self .__prefix = '$VAULTY;'
1396
+ self .__kcache = {}
1397
+
1398
+ def __derive_key (self , password , salt = None ):
1399
+ if (ckey := (password , salt )) in self .__kcache :
1400
+ e = self .__kcache [ckey ]
1401
+ self .__kcache [ckey ] = e [0 ], e [1 ], e [2 ] + 1
1402
+ return self .__kcache [ckey ]
1403
+
1404
+ if salt is None :
1405
+ salt = os .urandom (16 )
1406
+
1407
+ key = Scrypt (salt , 32 , 2 ** 16 , 8 , 1 ).derive (password .encode ('utf-8' ))
1408
+ self .__kcache [ckey ] = [salt , key , 0 ]
1409
+ return [salt , key , 0 ]
1410
+
1411
+ def encrypt (self , plaintext , password , cols = None ):
1412
+ version = b'\x01 '
1413
+ salt , key , uc = self .__derive_key (password )
1414
+ nonce = os .urandom (12 )[:- 4 ] + uc .to_bytes (4 , 'big' )
1415
+ ciphertext = ChaCha20Poly1305 (key ).encrypt (nonce , plaintext .encode ('utf-8' ), None )
1416
+
1417
+ r = self .__prefix + base64 .b64encode (version + salt + nonce + ciphertext ).decode ('utf-8' )
1418
+
1419
+ if cols is not None :
1420
+ r = '\n ' .join ([r [i :i + cols ] for i in range (0 , len (r ), cols )])
1421
+
1422
+ return r
1423
+
1424
+ def decrypt (self , ciphertext , password ):
1425
+ if ciphertext .lstrip ().startswith (self .__prefix ):
1426
+ try :
1427
+ nciphertext = base64 .b64decode (ciphertext .strip ()[len (self .__prefix ):])
1428
+
1429
+ if len (nciphertext ) > 29 and nciphertext .startswith (b'\x01 ' ):
1430
+ key = self .__derive_key (password , nciphertext [1 :17 ])[1 ]
1431
+ return ChaCha20Poly1305 (key ).decrypt (nciphertext [17 :29 ], nciphertext [29 :], None ).decode ('utf-8' )
1432
+
1433
+ except Exception :
1434
+ pass
1435
+
1436
+ raise JinjaFx .TemplateError ('invalid vaulty password or ciphertext malformed' )
1437
+
1438
+ raise JinjaFx .TemplateError ('data not encrypted with vaulty' )
1439
+
1440
+
1389
1441
if __name__ == '__main__' :
1390
1442
main ()
0 commit comments