Skip to content

Commit ffa069d

Browse files
committed
add all keys that can be decrypted by a credential to the encrypted keystore
1 parent 289c00e commit ffa069d

File tree

2 files changed

+111
-61
lines changed

2 files changed

+111
-61
lines changed

encryptcontent/decrypt-contents.tpl.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ function decrypt_key(password, iv_b64, ciphertext_b64, salt_b64) {
1212
let key = CryptoJS.AES.decrypt(encrypted, kdfkey, cfg);
1313

1414
try {
15-
let plaintext = key.toString(CryptoJS.enc.Latin1)
16-
if (plaintext.substr(32,1) == '#') {
17-
console.log(plaintext.substr(33)); //remember_suffix + keystore_id after the AES key
18-
return key.toString(CryptoJS.enc.Hex).substr(0,64); //first 32 bytes contain the AES key
15+
keystore = JSON.parse(key.toString(CryptoJS.enc.Utf8));
16+
if (encryptcontent_id in keystore) {
17+
return keystore[encryptcontent_id];
18+
} else {
19+
//id not found in keystore
20+
return false;
1921
}
2022
} catch (err) {
2123
// encoding failed; wrong password
@@ -38,7 +40,7 @@ function decrypt_key_from_bundle(password, ciphertext_bundle, username) {
3840
} else if (parts.length == 4 && username) {
3941
let userhash = CryptoJS.SHA256(encodeURIComponent(username.value)).toString(CryptoJS.enc.Base64);
4042
if (parts[3] == userhash) {
41-
return decrypt_key(username.value+password, parts[0], parts[1], parts[2]);
43+
return decrypt_key(password, parts[0], parts[1], parts[2]);
4244
}
4345
} else {
4446
return false;

encryptcontent/plugin.py

Lines changed: 104 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@
5353
base_path = os.path.dirname(os.path.abspath(__file__))
5454

5555

56+
KS_OBFUSCATE = -1
57+
KS_PASSWORD = 0
58+
5659
class encryptContentPlugin(BasePlugin):
5760
""" Plugin that encrypt markdown content with AES and inject decrypt form. """
5861

@@ -140,16 +143,33 @@ def __sign_file__(self, fname, url, key):
140143
signer = eddsa.new(key, 'rfc8032')
141144
return base64.b64encode(signer.sign(h)).decode()
142145

143-
def __encrypt_key__(self, key, password, iterations, id, user=''):
146+
def __add_to_keystore__(self, index, key, id, password, user=''):
147+
keystore = self.setup['keystore']
148+
store_id = id
149+
150+
if index not in keystore:
151+
new_entry = {}
152+
new_entry[store_id] = key.hex()
153+
keystore[index] = new_entry
154+
else:
155+
keystore[index][store_id] = key.hex()
156+
157+
def __encrypt_keys_from_keystore__(self, index):
158+
keystore = self.setup['keystore']
159+
password = index[1]
160+
if index[0] == KS_OBFUSCATE:
161+
iterations = 1
162+
else:
163+
iterations = self.setup['kdf_iterations']
144164
""" Encrypts key with PBKDF2 and AES-256. """
145165
salt = get_random_bytes(16)
146166
# generate PBKDF2 key from salt and password (password is URI encoded)
147-
kdfkey = PBKDF2(quote(user+password, safe='~()*!\''), salt, 32, count=iterations, hmac_hash_module=SHA256)
167+
kdfkey = PBKDF2(quote(password, safe='~()*!\''), salt, 32, count=iterations, hmac_hash_module=SHA256)
148168
# initialize AES-256
149169
iv = get_random_bytes(16)
150170
cipher = AES.new(kdfkey, AES.MODE_CBC, iv)
151171
# use it to encrypt the AES-256 key
152-
plaintext = key + '#'.encode() + quote(self.config['remember_suffix'] + str(id), safe='~()*!\'').encode()
172+
plaintext = json.dumps(keystore[index]).encode()
153173
# plaintext must be padded to be a multiple of 16 bytes
154174
plaintext_padded = pad(plaintext, 16, style='pkcs7')
155175
ciphertext = cipher.encrypt(plaintext_padded)
@@ -161,11 +181,21 @@ def __encrypt_key__(self, key, password, iterations, id, user=''):
161181
if self.setup['min_enttropy_secret'] == 0 or enttropy_secret < self.setup['min_enttropy_secret']:
162182
self.setup['min_enttropy_secret'] = enttropy_secret
163183

164-
return (
165-
base64.b64encode(iv).decode() ,
166-
base64.b64encode(ciphertext).decode(),
167-
base64.b64encode(salt).decode()
168-
)
184+
if isinstance(index[0], str):
185+
userhash = quote(index[0], safe='~()*!\'').encode() # safe transform username analogous to encodeURIComponent
186+
userhash = SHA256.new(userhash).digest() # sha256 sum of username
187+
return (
188+
base64.b64encode(iv).decode() ,
189+
base64.b64encode(ciphertext).decode(),
190+
base64.b64encode(salt).decode(),
191+
base64.b64encode(userhash).decode() # base64 encode userhash
192+
)
193+
else:
194+
return (
195+
base64.b64encode(iv).decode() ,
196+
base64.b64encode(ciphertext).decode(),
197+
base64.b64encode(salt).decode()
198+
)
169199

170200
def __encrypt_text__(self, text, key):
171201
""" Encrypts text with AES-256. """
@@ -198,28 +228,34 @@ def __encrypt_content__(self, content, base_path, encryptcontent_path, encryptco
198228
encryptcontent_id = ''
199229

200230
if encryptcontent['type'] == 'password':
201-
# get 32-bit AES-256 key from password_keystore
231+
# get 32-bit AES-256 key from password_keys
202232
key = encryptcontent['key']
203-
keystore = self.setup['password_keystore'][encryptcontent['password']]
204-
encryptcontent_id = quote(self.config['remember_suffix'] + str(keystore['id']), safe='~()*!\'')
205-
encryptcontent_keystore = keystore['store']
233+
keystore = self.setup['password_keys'][encryptcontent['password']]
234+
encryptcontent_id = keystore['id']
235+
encrypted_keystore = self.setup['keystore_password']
206236
elif encryptcontent['type'] == 'level':
207-
# get 32-bit AES-256 key from password_keystore
237+
# get 32-bit AES-256 key from level_keys
208238
key = encryptcontent['key']
209-
keystore = self.setup['level_keystore'][encryptcontent['level']]
210-
encryptcontent_id = quote(self.config['remember_suffix'] + str(keystore['id']), safe='~()*!\'')
211-
encryptcontent_keystore = keystore['store']
239+
keystore = self.setup['level_keys'][encryptcontent['level']]
240+
encryptcontent_id = keystore['id']
212241
if keystore.get('uname'):
242+
encrypted_keystore = self.setup['keystore_userpass']
213243
uname = 1
244+
else:
245+
encrypted_keystore = self.setup['keystore_password']
214246
elif encryptcontent['type'] == 'obfuscate':
215-
# get 32-bit AES-256 key from password_keystore
247+
# get 32-bit AES-256 key from obfuscate_keys
216248
key = encryptcontent['key']
217-
keystore = self.setup['obfuscate_keystore'][encryptcontent['obfuscate']]
218-
encryptcontent_id = quote(self.config['remember_suffix'] + str(keystore['id']), safe='~()*!\'')
219-
encryptcontent_keystore = keystore['store']
249+
keystore = self.setup['obfuscate_keys'][encryptcontent['obfuscate']]
250+
encryptcontent_id = keystore['id']
251+
encrypted_keystore = self.setup['keystore_obfuscate']
220252
obfuscate = 1
221253
obfuscate_password = encryptcontent['obfuscate']
222254

255+
encryptcontent_keystore = []
256+
for entry in encrypted_keystore:
257+
encryptcontent_keystore.append(encrypted_keystore[entry])
258+
223259
inject_something = encryptcontent['inject'] if 'inject' in encryptcontent else None
224260
delete_something = encryptcontent['delete_id'] if 'delete_id' in encryptcontent else None
225261

@@ -401,9 +437,14 @@ def on_config(self, config, **kwargs):
401437

402438
self.setup['kdf_iterations'] = pow(10,self.config['kdf_pow'])
403439

404-
self.setup['password_keystore'] = {}
405-
self.setup['obfuscate_keystore'] = {}
406-
self.setup['level_keystore'] = {}
440+
self.setup['password_keys'] = {}
441+
self.setup['obfuscate_keys'] = {}
442+
self.setup['level_keys'] = {}
443+
444+
self.setup['keystore'] = {}
445+
self.setup['keystore_password'] = {}
446+
self.setup['keystore_userpass'] = {}
447+
self.setup['keystore_obfuscate'] = {}
407448

408449
if self.config['password_file']:
409450
if self.config['password_inventory']:
@@ -417,31 +458,22 @@ def on_config(self, config, **kwargs):
417458
for level in self.config['password_inventory'].keys():
418459
new_entry = {}
419460
self.keystore_id += 1
420-
new_entry['id'] = self.keystore_id
461+
new_entry['id'] = quote(self.config['remember_suffix'] + str(self.keystore_id), safe='~()*!\'')
421462
new_entry['key'] = get_random_bytes(32)
422463
credentials = self.config['password_inventory'][level]
423464
if isinstance(credentials, list):
424-
new_entry['store'] = []
425465
for password in credentials:
426466
if isinstance(password, dict):
427467
logger.error("Configuration error in yaml syntax of 'password_inventory': expected string at level '{level}', but found dict!".format(level=level))
428468
os._exit(1)
429-
keystore = self.__encrypt_key__(new_entry['key'], password, self.setup['kdf_iterations'], self.keystore_id)
430-
new_entry['store'].append(';'.join(keystore))
469+
self.__add_to_keystore__((KS_PASSWORD,password), new_entry['key'], new_entry['id'], password)
431470
elif isinstance(credentials, dict):
432-
new_entry['store'] = []
433471
for user in credentials:
434472
new_entry['uname'] = user
435-
keystore = self.__encrypt_key__(new_entry['key'], credentials[user], self.setup['kdf_iterations'], self.keystore_id, user) # add username to password
436-
userhash = quote(user, safe='~()*!\'').encode() # safe transform username analogous to encodeURIComponent
437-
userhash = SHA256.new(userhash).digest() # sha256 sum of username
438-
userhash = base64.b64encode(userhash).decode() # base64 encode userhash
439-
new_entry['store'].append(';'.join(keystore + (userhash,)))
473+
self.__add_to_keystore__((user,credentials[user]), new_entry['key'], new_entry['id'], credentials[user], user)
440474
else:
441-
keystore = self.__encrypt_key__(new_entry['key'], credentials, self.setup['kdf_iterations'], self.keystore_id)
442-
new_entry['store'] = []
443-
new_entry['store'].append(';'.join(keystore))
444-
self.setup['level_keystore'][level] = new_entry
475+
self.__add_to_keystore__((KS_PASSWORD,password), new_entry['key'], new_entry['id'], credentials)
476+
self.setup['level_keys'][level] = new_entry
445477

446478
if self.config['sign_files']:
447479
sign_key_path = self.setup['config_path'].joinpath(self.config['sign_key'])
@@ -548,7 +580,7 @@ def on_page_markdown(self, markdown, page, config, **kwargs):
548580
# Set global password as default password for each page
549581
encryptcontent['password'] = self.config['global_password']
550582
# level '_global' will be set as global level
551-
if '_global' in self.setup['level_keystore']:
583+
if '_global' in self.setup['level_keys']:
552584
encryptcontent['level'] = '_global'
553585

554586
if 'password' in page.meta.keys():
@@ -594,34 +626,30 @@ def on_page_markdown(self, markdown, page, config, **kwargs):
594626
del page.meta['encryption_info_message']
595627

596628
if encryptcontent.get('password'):
597-
if encryptcontent['password'] not in self.setup['password_keystore']:
629+
if encryptcontent['password'] not in self.setup['password_keys']:
598630
new_entry = {}
599631
self.keystore_id += 1
600-
new_entry['id'] = self.keystore_id
632+
new_entry['id'] = quote(self.config['remember_suffix'] + str(self.keystore_id), safe='~()*!\'')
601633
new_entry['key'] = get_random_bytes(32)
602-
keystore = self.__encrypt_key__(new_entry['key'], encryptcontent['password'], self.setup['kdf_iterations'], self.keystore_id)
603-
new_entry['store'] = []
604-
new_entry['store'].append(';'.join(keystore))
605-
self.setup['password_keystore'][encryptcontent['password']] = new_entry
634+
self.__add_to_keystore__((KS_PASSWORD,password), new_entry['key'], new_entry['id'], encryptcontent['password'])
635+
self.setup['password_keys'][encryptcontent['password']] = new_entry
606636
encryptcontent['type'] = 'password'
607-
encryptcontent['key'] = self.setup['password_keystore'][encryptcontent['password']]['key']
637+
encryptcontent['key'] = self.setup['password_keys'][encryptcontent['password']]['key']
608638
setattr(page, 'encryptcontent', encryptcontent)
609639
elif encryptcontent.get('level'):
610640
encryptcontent['type'] = 'level'
611-
encryptcontent['key'] = self.setup['level_keystore'][encryptcontent['level']]['key']
641+
encryptcontent['key'] = self.setup['level_keys'][encryptcontent['level']]['key']
612642
setattr(page, 'encryptcontent', encryptcontent)
613643
elif encryptcontent.get('obfuscate'):
614-
if encryptcontent['obfuscate'] not in self.setup['obfuscate_keystore']:
644+
if encryptcontent['obfuscate'] not in self.setup['obfuscate_keys']:
615645
new_entry = {}
616646
self.keystore_id += 1
617-
new_entry['id'] = self.keystore_id
647+
new_entry['id'] = quote(self.config['remember_suffix'] + str(self.keystore_id), safe='~()*!\'')
618648
new_entry['key'] = get_random_bytes(32)
619-
keystore = self.__encrypt_key__(new_entry['key'], encryptcontent['obfuscate'], 1, self.keystore_id)
620-
new_entry['store'] = []
621-
new_entry['store'].append(';'.join(keystore))
622-
self.setup['obfuscate_keystore'][encryptcontent['obfuscate']] = new_entry
649+
self.__add_to_keystore__((KS_OBFUSCATE,encryptcontent['obfuscate']), new_entry['key'], new_entry['id'], encryptcontent['obfuscate'])
650+
self.setup['obfuscate_keys'][encryptcontent['obfuscate']] = new_entry
623651
encryptcontent['type'] = 'obfuscate'
624-
encryptcontent['key'] = self.setup['obfuscate_keystore'][encryptcontent['obfuscate']]['key']
652+
encryptcontent['key'] = self.setup['obfuscate_keys'][encryptcontent['obfuscate']]['key']
625653
setattr(page, 'encryptcontent', encryptcontent)
626654

627655
return markdown
@@ -664,6 +692,20 @@ def on_page_context(self, context, page, config, **kwargs):
664692
:param nav: global navigation object
665693
:return: dict of template context variables
666694
"""
695+
696+
# Encrypt all keys to keystore
697+
# It just encrypts once, but needs to run on every page
698+
for index in self.setup['keystore']:
699+
if index[0] == KS_OBFUSCATE:
700+
if index not in self.setup['keystore_obfuscate']:
701+
self.setup['keystore_obfuscate'][index] = ';'.join(self.__encrypt_keys_from_keystore__(index))
702+
elif index[0] == KS_PASSWORD:
703+
if index not in self.setup['keystore_password']:
704+
self.setup['keystore_password'][index] = ';'.join(self.__encrypt_keys_from_keystore__(index))
705+
else:
706+
if index not in self.setup['keystore_userpass']:
707+
self.setup['keystore_userpass'][index] = ';'.join(self.__encrypt_keys_from_keystore__(index))
708+
667709
if hasattr(page, 'encryptcontent'):
668710
if 'i18n_page_file_locale' in context:
669711
locale = context['i18n_page_file_locale']
@@ -806,6 +848,7 @@ def on_post_build(self, config, **kwargs):
806848
807849
:param config: global configuration object
808850
"""
851+
809852
Path(config.data["site_dir"] + '/assets/javascripts/').mkdir(parents=True, exist_ok=True)
810853
decrypt_js_path = Path(config.data["site_dir"] + '/assets/javascripts/decrypt-contents.js')
811854
with open(decrypt_js_path, "w") as file:
@@ -826,9 +869,14 @@ def on_post_build(self, config, **kwargs):
826869
new_entry['url'] = "https:" + jsurl[0]
827870
self.setup['files_to_sign'].append(new_entry)
828871

829-
self.setup['password_keystore'].clear()
830-
self.setup['obfuscate_keystore'].clear()
831-
self.setup['level_keystore'].clear()
872+
#clear all keystores
873+
self.setup['password_keys'].clear()
874+
self.setup['obfuscate_keys'].clear()
875+
self.setup['level_keys'].clear()
876+
self.setup['keystore'].clear()
877+
self.setup['keystore_obfuscate'].clear()
878+
self.setup['keystore_password'].clear()
879+
self.setup['keystore_userpass'].clear()
832880

833881
#modify search_index in the style of mkdocs-exclude-search
834882
if self.setup['search_plugin_found'] and self.config['search_index'] != 'clear':

0 commit comments

Comments
 (0)