diff --git a/.github/workflows/alfred.yml b/.github/workflows/alfred.yml new file mode 100644 index 0000000..e2c1286 --- /dev/null +++ b/.github/workflows/alfred.yml @@ -0,0 +1,26 @@ +name: Create Alfred Workflow + +on: + push: + tags: + - 'v*' + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Build Alfred workflow + id: builder + uses: almibarss/build-alfred-workflow@v1 + with: + workflow_dir: ente-totp + exclude_patterns: '*.pyc *__pycache__/*' + custom_version: ${{ github.ref_name }} + + - name: Release + uses: softprops/action-gh-release@v1 + with: + files: ${{ steps.builder.outputs.workflow_file }} \ No newline at end of file diff --git a/ente-totp/.gitignore b/ente-totp/.gitignore index 6489ade..6c7f56d 100644 --- a/ente-totp/.gitignore +++ b/ente-totp/.gitignore @@ -17,4 +17,9 @@ venv/ ENV/ env.bak/ venv.bak/ +.envrc + +# ide files +.idea/ + diff --git a/ente-totp/.tool-versions b/ente-totp/.tool-versions new file mode 100644 index 0000000..1d7a709 --- /dev/null +++ b/ente-totp/.tool-versions @@ -0,0 +1 @@ +python 3.12.3 diff --git a/ente-totp/README.md b/ente-totp/README.md index 8837781..f94de36 100644 --- a/ente-totp/README.md +++ b/ente-totp/README.md @@ -1,8 +1,14 @@ # An Alfred Workflow that uses your Ente Exports -Ente Auth does [not support](https://github.com/ente-io/ente/discussions/716) exporting TOTP codes. To use this project, please export from the Ente app and then manually import them into the workflow's database using the `import` parameter. +Ente Auth CLI does [not support](https://github.com/ente-io/ente/discussions/716) exporting TOTP codes. To use this project, please export from the Ente app and then +and then import them into the workflow's database by choosing the export file using the "Configure Workflow" button +in the workflow setup and then import using the 'ente import' Alfred command. -> [!NOTE] **Note**: In the future, the workflow will take care of the import. +The file can be deleted once imported. + +> [!NOTE] +> In the future, the workflow will take care of the import. +> In addtion, once support for exporting codes via CLI is supported, we will use that instead. ## Setup @@ -13,16 +19,6 @@ Ente Auth does [not support](https://github.com/ente-io/ente/discussions/716) ex 1. Open Alfred 2. Go to workflows -3. Right click the Ente 2FA workflow and press Open In Finder. -4. Export in plain text your Ente 2FA codes -5. Rename that file to secrets.txt and put it in the folder that got opened previously - -![image](https://github.com/user-attachments/assets/0964cb6c-e453-4be9-a8a8-2891001f2762) - -It should look something like this - -6. Open a terminal -7. Navigate to the folder that got contains the `main.py` file -8. Run `python main.py import secrets.txt` -9. Run `ente` in Alfred and let the database populate - +3. Click the Ente 2FA workflow and click the Configure Workflow button +4. Click the file button next to the Ente Export File and browse to the Ente Auth plain text export of your two factor codes +5. Run the alfred command 'ente import' \ No newline at end of file diff --git a/ente-totp/info.plist b/ente-totp/info.plist index d1691cb..dab483e 100644 --- a/ente-totp/info.plist +++ b/ente-totp/info.plist @@ -4,23 +4,8 @@ bundleid - category - Tools connections - 0E38FB93-F4AB-4414-AFE5-E10B80176C38 - - - destinationuid - 61342121-5F6E-4340-A89B-F4DC5D5A7965 - modifiers - 0 - modifiersubtext - - vitoclose - - - 548A4F34-31FF-4E4B-AD7C-1CA4D8391AAA @@ -58,27 +43,6 @@ Ente TOTP Codes objects - - config - - argumenttype - 0 - keyword - {var:keyword} - subtext - Getting TOTP Codes... - text - {const:alfred_workflow_name} - withspace - - - type - alfred.workflow.input.keyword - uid - 0E38FB93-F4AB-4414-AFE5-E10B80176C38 - version - 1 - config @@ -95,11 +59,11 @@ escaping 102 keyword - ente + {var:keyword} queuedelaycustom 3 queuedelayimmediatelyinitially - + queuedelaymode 0 queuemode @@ -159,11 +123,11 @@ focusedappvariablename hotkey - 0 + 14 hotmod - 0 + 1703936 hotstring - + E leftcursor modsmode @@ -178,31 +142,83 @@ version 2 + + config + + alfredfiltersresults + + alfredfiltersresultsmatchmode + 0 + argumenttreatemptyqueryasnil + + argumenttrimmode + 0 + argumenttype + 1 + escaping + 102 + keyword + ente import + queuedelaycustom + 10 + queuedelayimmediatelyinitially + + queuedelaymode + 2 + queuemode + 1 + runningsubtext + importing.... + script + python3 main.py import $export_file_path + scriptargtype + 1 + scriptfile + + subtext + + title + import your ente data + type + 0 + withspace + + + type + alfred.workflow.input.scriptfilter + uid + 3101A957-02C3-4D0C-8B13-1C3721459261 + version + 3 + readme - + An Alfred Workflow that uses your Ente Exports +Ente Auth CLI does not support exporting TOTP codes. To use this project, please export from the Ente app and then import them into the workflow's database by choosing the export file using the "Configure Workflow" button in the workflow setup and then import using the 'ente import' Alfred command. + +The file can be deleted once imported. uidata - 0E38FB93-F4AB-4414-AFE5-E10B80176C38 + 3101A957-02C3-4D0C-8B13-1C3721459261 xpos - 40 + 250 ypos - 75 + 330 548A4F34-31FF-4E4B-AD7C-1CA4D8391AAA xpos - 40 + 30 ypos - 220 + 305 61342121-5F6E-4340-A89B-F4DC5D5A7965 xpos - 310 + 240 ypos - 140 + 105 89FF22C4-362B-4675-AEFA-B15E67898E1B @@ -218,7 +234,7 @@ config default - code||totp||en + code||totp||en||ea placeholder en required @@ -235,7 +251,68 @@ variable keyword + + config + + default + + filtermode + 2 + placeholder + plain text Ente Auth export + required + + + description + Point this to the plain text export file from Ente Auth containing your 2FA data. It can be deleted after initial import. + label + Ente Export File + type + filepicker + variable + export_file_path + + + config + + default + + required + + text + + + description + + label + include username in results title + type + checkbox + variable + username_in_title + + + config + + default + + required + + text + + + description + + label + include username in results subtitle + type + checkbox + variable + username_in_subtitle + + variablesdontexport + version webaddress diff --git a/ente-totp/main.py b/ente-totp/main.py index 62fd163..841ff32 100755 --- a/ente-totp/main.py +++ b/ente-totp/main.py @@ -1,5 +1,7 @@ import json +import logging import pathlib +import os from collections import defaultdict from datetime import datetime, timedelta @@ -7,6 +9,8 @@ import pyotp DB_FILE = pathlib.Path.home() / ".local/share/ente-totp/db.json" +USERNAME_IN_TITLE = os.getenv("username_in_title", "false").lower() in ("true", "1", "t", "y", "yes") +USERNAME_IN_SUBTITLE = os.getenv("username_in_subtitle", "false").lower() in ("true", "1", "t", "y", "yes") @click.group() @@ -15,15 +19,29 @@ def cli(): @cli.command("import") -@click.argument("file", type=click.Path(exists=True)) +@click.argument("file", type=click.Path(exists=False), required=False) def import_file(file): - secret_dict = defaultdict(list) # less strict than regular dict - for service_name, username, secret in parse_secrets(file): - secret_dict[service_name].append((username, secret)) - DB_FILE.parent.mkdir(parents=True, exist_ok=True) - with DB_FILE.open("w") as json_file: - json.dump(secret_dict, json_file, indent=2) - print("Database created.") + try: + logging.warning(f"import_file: {file}") + secret_dict = defaultdict(list) # less strict than regular dict + for service_name, username, secret in parse_secrets(file): + secret_dict[service_name].append((username, secret)) + DB_FILE.parent.mkdir(parents=True, exist_ok=True) + with DB_FILE.open("w") as json_file: + json.dump(secret_dict, json_file, indent=2) + + logging.warning(f"Database created with {sum(len(v) for v in secret_dict.values())} entries.") + + print(json.dumps({"items": [{"title": "Import Successful", + "subtitle": f"Database created with {sum(len(v) for v in secret_dict.values())} entries."}]})) + except FileNotFoundError: + error_message = f"File not found: {file}" + logging.error(error_message) + print(json.dumps({"items": [{"title": "Import Failed", "subtitle": error_message}]})) + except Exception as e: + error_message = f"An error occurred: {str(e)}" + logging.error(error_message) + print(json.dumps({"items": [{"title": "Import Failed", "subtitle": error_message}]})) def parse_secrets(file_path="secrets.txt"): @@ -33,24 +51,29 @@ def parse_secrets(file_path="secrets.txt"): for line in secrets_file: line = line.strip() if line: - line = line.replace("sha", "SHA").split("codeDisplay")[0][:-1] + line = line.replace("=sha1", "=SHA1") + if "codeDisplay" in line: + line = line.split("codeDisplay")[0][:-1] + parsed_uri = pyotp.parse_uri(line) if parsed_uri: - service_name = parsed_uri.issuer + service_name = parsed_uri.issuer or parsed_uri.name username = parsed_uri.name secret = parsed_uri.secret if secret: secrets_list.append((service_name, username, secret)) - + else: + print(f"Unable to parse secret in: {line}") + else: + print(f"Unable to parse the line: {line}") return secrets_list def format_data(service_name, username, current_totp, next_totp, output_type): """Format the data based on the output type.""" - if username: - subset = f"Current TOTP: {current_totp} | Next TOTP: {next_totp} - {username}" - else: - subset = f"Current TOTP: {current_totp} | Next TOTP: {next_totp}" + subset = f"Current TOTP: {current_totp} | Next TOTP: {next_totp}" + ( + f" - {username}" if username and USERNAME_IN_SUBTITLE else "") + service_name = f"{service_name} - {username}" if username and USERNAME_IN_TITLE else service_name if output_type == "alfred": return { @@ -85,6 +108,7 @@ def generate_totp(secret_id, output_format): with open(DB_FILE, "r") as file: data = json.load(file) items = [] # Collect all items in this list + logging.warning(f"Searching for {secret_id} in {len(data)} services.\n") for service_name, service_data in data.items(): if secret_id.lower() in service_name.lower(): for username, secret in service_data: @@ -103,6 +127,7 @@ def generate_totp(secret_id, output_format): print("No matching services found.") except Exception as e: + logging.warning(f"Error: {str(e)}") print(json.dumps({"items": [], "error": str(e)}, indent=4))