Python script to reveal an unlocked file for encrypted files in a vault (and vice versa!)

Update: It works in both directions now :star_struck: For more info, see my new Github repository

Orginal Post:

Warning upfront: this post is only relevant for power users on Windows who know what they’re doing. I’m quite reluctant to guide non-programmers through the process of installing all prerequisites to run this script and deal with the consequences when something goes wrong. So: use at your own risk!

Background: after doing a sync between two PCs yesterday I got a sync error on an encrypted vault file. Apparently the file was modified on both PCs before the sync. To solve it, I’d need to know which decrypted file it represents, so I can inspect the file on both ends to fix the conflict manually.

However, since there is no way to map encrypted files in a vault to their decrypted file paths, that’s a dead stop in fixing such issues. I’ve been hit by this issue a couple of times in the past already. So far I resorted to a manual procedure to find out which file(s) were involved;

  • unlock the vault on one PC,
  • open a command prompt and write a recursive dir listing for the unlocked vault into a text file,
  • browse to the conflicting encrypted file in Cryptomator’s encrypted storage folder,
  • move the file over to e.g. the desktop,
  • re-do the dir listing to another text file,
  • restore the encrypted file back to it’s proper location,
  • diff the two dir listing text files to see which file is absent in one of them.

Quite a tedious procedure, especially since the dir command also lists the special ‘.’ and ‘…’ folder entries, but uses the current time as their time stamp. Since the two dir listing files are not created at exactly the same time, all these entries cause false positives in the diff. So for large vaults I also preprocessed the text files to strip out these lines with a regex.

And yesterday was the time I decided “enough is enough - this can be scripted”. So, see below the Python script that does this process for you. You need to have Python 3 installed with the win32com and wx packages. See the source code for the full instructions on how to use the script.

# Cryptomator Vault File Revealer
# Reveals the decrypted file which corresponds with an encrypted file
# in a locked Cryptomator vault.
# Created by Carl Colijn
# Warning: use at your own risk!
# Instructions and notes:
# - This script requires Python 3, as well as the following modules:
#   - wx
#   - win32com
# - This script only works on Windows; feel free to adapt it to other
#   OSes and share your result!
# - Before starting this script, unlock the vault in Cryptomator first.
# - The script works by temporarily moving the selected encrypted file
#   to the side so that Cryptomator doesn't recognize it anymore.  The
#   script does a dir dump on the unlocked vault both before and after
#   moving the encrypted file; the difference in the dumps is the file
#   which was moved aside in the encrypted vault.
# - Might something go wrong: the encrypted file is not moved to another
#   location, but it is renamed by adding the extension '.cvfr-sidestepped'
#   to it.  This makes Crytpomator not recognize the file anymore, which
#   makes it disappear from the unlocked vault.  So if the script fails
#   and doesn't restore the encrypted file anymore, find the renamed file
#   and manually rename it back to what it should be named (remove the
#   added extension).
# - IMPORTANT NOTE: I only tested it on regular encrypted file entries,
#   and not on encrypted folder entries.  Renaming those seems rather iffy
#   to me; will Cryptomator handle that silently without issue, or could
#   it mess up the vault structure in such a way that the vault gets
#   corrupted?  I've not felt the need to find out yet :)  Feel free to
#   find out at your own risk and tell us the result!

from import shell, shellcon
import os
import wx
import subprocess

def GetDocumentsPath():
  return shell.SHGetFolderPath(0, shellcon.CSIDL_PERSONAL, None, 0)

def BrowseDecryptedFolder(defaultPath):
  browseDlg = wx.DirDialog(None, 'Unlock the vault in Cryptomator and browse to the unlocked folder', defaultPath=defaultPath, style=wx.DD_DEFAULT_STYLE)
  if browseDlg.ShowModal() == wx.ID_OK:
    result = browseDlg.GetPath()
    result = None
  return result

def BrowseEncryptedFile(defaultPath):
  browseDlg = wx.FileDialog(None, 'Select the encrypted file in the locked vault folder', defaultDir=defaultPath, style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST)
  if browseDlg.ShowModal() == wx.ID_OK:
    result = browseDlg.GetPath()
    result = None
  return result

def GetFilePathsInFolder(folderPath):
  filePaths = set()
  for rootFolderPath, folderNames, fileNames in os.walk(folderPath):
    for fileName in fileNames:
      filePaths.add(os.path.join(rootFolderPath, fileName))
  return filePaths

def DisableEncryptedFile(encryptedFilePath):
  tempFilePath = encryptedFilePath + '.cvfr-sidestepped'
  os.rename(encryptedFilePath, tempFilePath)
  return tempFilePath

def EnableEncryptedFile(tempFilePath, encryptedFilePath):
  os.rename(tempFilePath, encryptedFilePath)

def TellFileNotFound():
  dlg = wx.MessageDialog(None, 'I cannot find the corresponding decrypted file!  Maybe you unlocked the wrong vault?', 'File not found', wx.OK | wx.ICON_INFORMATION)

def TellFileFound(decryptedFilePath):
  dlg = wx.MessageDialog(None, 'The corresponding decrypted file is:\n' + decryptedFilePath + '\n\nDo you want to reveal this file in Explorer?', 'File found', wx.YES_NO | wx.YES_DEFAULT | wx.ICON_INFORMATION)
  result = dlg.ShowModal()
  if result == wx.ID_YES:
    explorerPath = os.path.join(os.getenv('WINDIR'), 'explorer.exe')[explorerPath, '/select,', decryptedFilePath])

def AskFindOtherFile():
  dlg = wx.MessageDialog(None, 'Do you want to reveal another file?', 'Reveal another', wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION)
  result = dlg.ShowModal()
  return result == wx.ID_YES

def FindMissingFile(decryptedFolderPath, allFilePaths):
  for filePath in allFilePaths:
    if not os.path.isfile(filePath):
      return filePath
  return None

def RevealDecryptedFile(allFilePaths, decryptedFolderPath, encryptedFilePath):
  tempFilePath = DisableEncryptedFile(encryptedFilePath)
    decryptedFilePath = FindMissingFile(decryptedFolderPath, allFilePaths)
    EnableEncryptedFile(tempFilePath, encryptedFilePath)

  if decryptedFilePath is None:

class MyApp(wx.App):
  def OnInit(self):
    startPath = GetDocumentsPath()

    decryptedFolderPath = BrowseDecryptedFolder(startPath)
    if decryptedFolderPath is None:
      return True

    allFilePaths = GetFilePathsInFolder(decryptedFolderPath)

    while True:
      encryptedFilePath = BrowseEncryptedFile(startPath)
      if encryptedFilePath is None:
        return True

      RevealDecryptedFile(allFilePaths, decryptedFolderPath, encryptedFilePath)

      if not AskFindOtherFile():
        return True

app = MyApp(0)

Thank you for that script.
Out of curiosity:
What you describe is a classic sync conflict that is usually treated by the storage provider by creating 2 files so one can check both files and decide which one to keep, or merge them.
According to this post, Cryptomator can detect this and should also show 2 unencrypted files so one can choose/merge.
Is this not working in your scenario?
Or did I get the purpose of your script wrong?

No, I don’t use Cryptomator in the classic sense with a single vault located directly in a cloud synced location. I instead use it to keep confidential data from my freelance projects safe from data theft by having them always in ‘cold storage’ until I need to work on a specific project, at which time I specifically unlock only that project’s vault. So all data is always encrypted 99% of the time, even on my own computer. But I also use it in that way to manage vaults with private data, and these vaults get cross-synced over the internal lan to other PCs in this household. And so I sometimes run into issues when some files get modified on more than one PC between syncs. Knowing which files are in conflict is then crucial to resolve the issue; hence the script :slight_smile:

But to come back to your link: that seems very useful in my situation! If I can augment the sync script to just keep both files but rename one of them instead of skipping them and issuing a conflict afterwards, I can then just do a search afterwards for files with the (Conflict …) indicator in their filename, which makes this script unnecessary.

1 Like

And I’m back :slight_smile: This time I needed the reverse of what the script does; I lost a single file and needed to restore it from backup, while not restoring the rest of the vault from backup too. Since the mechanism for revealing an encrypted file given a decrypted file is rather similar, I upgraded the script to be used in both directions.

I’ve also decided to host the new script on GitHub; that way this post won’t get overly long with inlined scripts, plus future updates will be at a known location. You can find it at Cryptomator-Vault-File-Revealer!


Hi @CarlColijn ,

Thanks for the great idea!!

I wrote another version of this idea if that can help (works on macos with python 3.10):

from pathlib import Path
from contextlib import contextmanager
from tempfile import TemporaryDirectory
import shutil

from tqdm import tqdm

def temporary_move_file(path):
    with TemporaryDirectory() as temp_dir:
        temp_path = Path(temp_dir) /
        shutil.move(path, temp_path)
        shutil.move(temp_path, path)

def get_encrypted_path(unencrypted_path, encrypted_dir):
    """Given an unencrypted path, it will return the path to its encrypted version.

    `get_encrypted_path("my_unencrypted_file.txt", "my_encrypted_cryptomator_dir")`
    unencrypted_path = Path(unencrypted_path)
    encrypted_dir = Path(encrypted_dir)
    original_encrypted_files = list(tqdm(encrypted_dir.rglob("*"), desc="Listing encrypted files"))
    with temporary_move_file(unencrypted_path):
        encrypted_files_with_file_removed = list(tqdm(encrypted_dir.rglob("*"), desc=f"Listing encrypted files with {} removed"))
    [encrypted_path] = set(original_encrypted_files) - set(encrypted_files_with_file_removed)
    return encrypted_path
1 Like

Thanks for that! I didn’t find a use for tqdm since my script is GUI based and I try to avoid extra external modules, but I happily took over your use of pathlib.Path.rglob (didn’t know it existed) and a context manager instead of my try/finally.

© 2022 Skymatic GmbH • Privacy PolicyImpressum