Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ are restored automatically.
<table>
<tr>
<td><img src="docs/assets/pdrive-account-settings.png" alt="PDrive Preferences account section with the guarded Change Proton account action"></td>
<td><img src="docs/assets/pdrive-account-switch.png" alt="PDrive guarded Proton account-change dialog after a successful safety preflight"></td>
<td><img src="docs/assets/pdrive-account-switch.png?v=0.7.7" alt="PDrive guarded Proton account-change dialog after a successful safety preflight"></td>
</tr>
<tr>
<td align="center"><sub>A deliberate account action, separate from contextual reauthorization</sub></td>
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.7.6
0.7.7
10 changes: 3 additions & 7 deletions bin/pdrive-account-switch
Original file line number Diff line number Diff line change
Expand Up @@ -84,15 +84,11 @@ fi
read -r -p 'New Proton username or email address: ' proton_username
read -r -s -p 'New Proton password: ' proton_password
printf '\n'
read -r -s -p 'Repeat password: ' proton_password_confirm
printf '\n'
if [[ -z "${proton_username}" || -z "${proton_password}" \
|| "${proton_password}" != "${proton_password_confirm}" ]]; then
unset proton_password proton_password_confirm
printf 'Username is empty or passwords do not match.\n' >&2
if [[ -z "${proton_username}" || -z "${proton_password}" ]]; then
unset proton_password
printf 'Username and password must not be empty.\n' >&2
exit 65
fi
unset proton_password_confirm
read -r -s -p 'Fresh 6-digit 2FA code (empty when disabled): ' proton_2fa
printf '\n'
if [[ -n "${proton_2fa}" && ! "${proton_2fa}" =~ ^[0-9]{6}$ ]]; then
Expand Down
9 changes: 0 additions & 9 deletions bin/pdrive-reauth
Original file line number Diff line number Diff line change
Expand Up @@ -94,15 +94,6 @@ if [[ -z "${proton_password}" ]]; then
echo 'The password must not be empty.' >&2
exit 65
fi
read -r -s -p 'Repeat password: ' proton_password_confirm
echo
if [[ "${proton_password}" != "${proton_password_confirm}" ]]; then
unset proton_password proton_password_confirm
echo 'The passwords do not match.' >&2
exit 65
fi
unset proton_password_confirm

read -r -s -p 'Current 6-digit 2FA code (empty when 2FA is disabled): ' proton_2fa
echo
if [[ -n "${proton_2fa}" && ! "${proton_2fa}" =~ ^[0-9]{6}$ ]]; then
Expand Down
2 changes: 1 addition & 1 deletion bin/pdrive-state
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ from typing import Any


SCHEMA_VERSION = 1
TOOL_VERSION = "0.7.6"
TOOL_VERSION = "0.7.7"
RECENT_TRANSFER_WINDOW_SECONDS = 24 * 60 * 60
RECENT_TRANSFER_LIMIT = 24
MOUNT_LOG_TAIL_BYTES = 512 * 1024
Expand Down
47 changes: 17 additions & 30 deletions bin/pdrive-ui
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ except (ImportError, ValueError):


APP_ID = "io.github.claudiuschuster.PDriveControl"
VERSION = "0.7.6"
VERSION = "0.7.7"
REFRESH_INTERVAL_SECONDS = 2
REFRESH_INTERVAL_OPTIONS = (1, 2, 5, 10)
GRAPH_WINDOW_SECONDS = 5 * 60
Expand Down Expand Up @@ -532,15 +532,14 @@ GERMAN_TRANSLATIONS = {
"Proton account": "Proton-Konto",
"Username or email": "Benutzername oder E-Mail",
"Password": "Passwort",
"Repeat password": "Passwort wiederholen",
"Current 2FA code (optional)": "Aktueller 2FA-Code (optional)",
"Use a fresh six-digit code. Leave this empty when two-factor authentication is disabled.": "Verwende einen frischen sechsstelligen Code. Lass das Feld leer, wenn Zwei-Faktor-Authentifizierung deaktiviert ist.",
"Show passwords": "Passwörter anzeigen",
"Show password and 2FA code": "Passwort und 2FA-Code anzeigen",
"Back": "Zurück",
"Connect securely": "Sicher verbinden",
"Credentials are sent through a private anonymous pipe, never through process arguments, environment variables or logs.": "Zugangsdaten werden durch eine private anonyme Pipe übertragen, niemals über Prozessargumente, Umgebungsvariablen oder Logs.",
"Enter your Proton username or email address.": "Gib deinen Proton-Benutzernamen oder deine E-Mail-Adresse ein.",
"Passwords are empty or do not match.": "Die Passwörter sind leer oder stimmen nicht überein.",
"Enter your Proton password.": "Gib dein Proton-Passwort ein.",
"The 2FA code must be empty or exactly six digits.": "Der 2FA-Code muss leer oder genau sechsstellig sein.",
"Reauthorization required": "Neue Autorisierung erforderlich",
"Proton account reauthorization is required. Automatic login retries were stopped to protect the account.": "Das Proton-Konto muss neu autorisiert werden. Automatische Loginversuche wurden zum Schutz des Kontos gestoppt.",
Expand Down Expand Up @@ -568,7 +567,6 @@ GERMAN_TRANSLATIONS = {
"Checking active transfers, queued uploads and protected Dirty cache data …": "Aktive Übertragungen, eingereihte Uploads und geschützte Dirty-Cachedaten werden geprüft …",
"New username or email": "Neuer Benutzername oder E-Mail",
"New password": "Neues Passwort",
"Repeat new password": "Neues Passwort wiederholen",
"Fresh 2FA code (optional)": "Frischer 2FA-Code (optional)",
"Credentials travel through one anonymous pipe. They never enter process arguments, environment variables, logs, diagnostics or screenshots.": "Zugangsdaten laufen durch eine anonyme Pipe. Sie gelangen niemals in Prozessargumente, Umgebungsvariablen, Logs, Diagnosen oder Screenshots.",
"I understand that /pdrive will show the new account and that previous cache data remains separate.": "Ich verstehe, dass /pdrive das neue Konto zeigt und bisherige Cachedaten getrennt bleiben.",
Expand All @@ -581,6 +579,7 @@ GERMAN_TRANSLATIONS = {
"Account safety could not be verified. The current account and cache remain unchanged.": "Die Kontosicherheit konnte nicht geprüft werden. Das aktuelle Konto und der Cache bleiben unverändert.",
"Account changes are disabled in demo mode.": "Kontowechsel sind im Demo-Modus deaktiviert.",
"Enter the new Proton username or email address.": "Gib den neuen Proton-Benutzernamen oder die E-Mail-Adresse ein.",
"Enter the new Proton password.": "Gib das Passwort des neuen Proton-Kontos ein.",
"Confirm the intentional Proton account migration.": "Bestätige den beabsichtigten Proton-Kontowechsel.",
"Proton account changed": "Proton-Konto gewechselt",
"/pdrive is mounted through the new account’s separate cache namespace.": "/pdrive ist über den getrennten Cache-Namensraum des neuen Kontos gemountet.",
Expand Down Expand Up @@ -2702,11 +2701,9 @@ class SetupWizard(Gtk.Box):
self.username_entry = Gtk.Entry()
self.username_entry.set_input_purpose(Gtk.InputPurpose.EMAIL)
self.password_entry = Gtk.Entry()
self.password_confirm_entry = Gtk.Entry()
self.two_factor_entry = Gtk.Entry()
for entry in (
self.password_entry,
self.password_confirm_entry,
self.two_factor_entry,
):
entry.set_visibility(False)
Expand All @@ -2717,7 +2714,6 @@ class SetupWizard(Gtk.Box):
(
("Username or email", self.username_entry),
("Password", self.password_entry),
("Repeat password", self.password_confirm_entry),
("Current 2FA code (optional)", self.two_factor_entry),
)
):
Expand All @@ -2731,8 +2727,8 @@ class SetupWizard(Gtk.Box):
)
two_factor_note.set_line_wrap(True)
page.pack_start(two_factor_note, False, False, 0)
show_passwords = Gtk.CheckButton(label=translate("Show passwords"))
show_passwords.set_tooltip_text(translate("Show passwords"))
show_passwords = Gtk.CheckButton(label=translate("Show password and 2FA code"))
show_passwords.set_tooltip_text(translate("Show password and 2FA code"))
show_passwords.connect("toggled", self.on_show_passwords)
page.pack_start(show_passwords, False, False, 0)
self.account_error = label("", "setup-error")
Expand Down Expand Up @@ -3058,7 +3054,7 @@ class SetupWizard(Gtk.Box):
def on_show_passwords(self, button: Gtk.CheckButton) -> None:
visible = button.get_active()
self.password_entry.set_visibility(visible)
self.password_confirm_entry.set_visibility(visible)
self.two_factor_entry.set_visibility(visible)

def show_account_error(self, message: str) -> None:
self.account_error.set_text(translate(message))
Expand All @@ -3069,13 +3065,12 @@ class SetupWizard(Gtk.Box):
return
username = self.username_entry.get_text().strip()
password = self.password_entry.get_text()
confirmation = self.password_confirm_entry.get_text()
two_factor = self.two_factor_entry.get_text().strip()
if not username:
self.show_account_error("Enter your Proton username or email address.")
return
if not password or password != confirmation:
self.show_account_error("Passwords are empty or do not match.")
if not password:
self.show_account_error("Enter your Proton password.")
return
if two_factor and not re.fullmatch(r"[0-9]{6}", two_factor):
self.show_account_error("The 2FA code must be empty or exactly six digits.")
Expand All @@ -3085,12 +3080,11 @@ class SetupWizard(Gtk.Box):
payload.extend(value.encode("utf-8"))
payload.append(0)
self.password_entry.set_text("")
self.password_confirm_entry.set_text("")
self.two_factor_entry.set_text("")
self.account_error.hide()
self.connecting = True
self.stack.set_visible_child_name("progress")
del password, confirmation, two_factor
del password, two_factor
threading.Thread(target=self.connect_worker, args=(payload,), daemon=True).start()

def connect_worker(self, payload: bytearray) -> None:
Expand Down Expand Up @@ -3241,8 +3235,8 @@ class ReauthorizationDialog(Gtk.Dialog):
retry_note.set_line_wrap(True)
content.pack_start(retry_note, False, False, 0)

self.show_password = Gtk.CheckButton(label=translate("Show passwords"))
self.show_password.set_tooltip_text(translate("Show passwords"))
self.show_password = Gtk.CheckButton(label=translate("Show password and 2FA code"))
self.show_password.set_tooltip_text(translate("Show password and 2FA code"))
self.show_password.connect("toggled", self.on_show_password)
content.pack_start(self.show_password, False, False, 0)

Expand Down Expand Up @@ -3511,11 +3505,9 @@ class AccountSwitchDialog(Gtk.Dialog):
self.username_entry = Gtk.Entry()
self.username_entry.set_input_purpose(Gtk.InputPurpose.EMAIL)
self.password_entry = Gtk.Entry()
self.password_confirm_entry = Gtk.Entry()
self.two_factor_entry = Gtk.Entry()
for entry in (
self.password_entry,
self.password_confirm_entry,
self.two_factor_entry,
):
entry.set_visibility(False)
Expand All @@ -3526,7 +3518,6 @@ class AccountSwitchDialog(Gtk.Dialog):
(
("New username or email", self.username_entry),
("New password", self.password_entry),
("Repeat new password", self.password_confirm_entry),
("Fresh 2FA code (optional)", self.two_factor_entry),
)
):
Expand All @@ -3553,8 +3544,8 @@ class AccountSwitchDialog(Gtk.Dialog):
self.confirmation.connect("toggled", self.update_button_state)
content.pack_start(self.confirmation, False, False, 0)

self.show_password = Gtk.CheckButton(label=translate("Show passwords"))
self.show_password.set_tooltip_text(translate("Show passwords"))
self.show_password = Gtk.CheckButton(label=translate("Show password and 2FA code"))
self.show_password.set_tooltip_text(translate("Show password and 2FA code"))
self.show_password.connect("toggled", self.on_show_password)
content.pack_start(self.show_password, False, False, 0)

Expand Down Expand Up @@ -3646,15 +3637,13 @@ class AccountSwitchDialog(Gtk.Dialog):
def on_show_password(self, button: Gtk.CheckButton) -> None:
visible = button.get_active()
self.password_entry.set_visibility(visible)
self.password_confirm_entry.set_visibility(visible)
self.two_factor_entry.set_visibility(visible)

def credentials_valid(self) -> bool:
two_factor = self.two_factor_entry.get_text().strip()
return (
bool(self.username_entry.get_text().strip())
and bool(self.password_entry.get_text())
and self.password_entry.get_text() == self.password_confirm_entry.get_text()
and (not two_factor or bool(re.fullmatch(r"[0-9]{6}", two_factor)))
)

Expand Down Expand Up @@ -3687,13 +3676,12 @@ class AccountSwitchDialog(Gtk.Dialog):
return
username = self.username_entry.get_text().strip()
password = self.password_entry.get_text()
password_confirm = self.password_confirm_entry.get_text()
two_factor = self.two_factor_entry.get_text().strip()
if not username:
self.show_error("Enter the new Proton username or email address.")
return
if not password or password != password_confirm:
self.show_error("Passwords are empty or do not match.")
if not password:
self.show_error("Enter the new Proton password.")
return
if two_factor and not re.fullmatch(r"[0-9]{6}", two_factor):
self.show_error("The 2FA code must be empty or exactly six digits.")
Expand All @@ -3708,9 +3696,8 @@ class AccountSwitchDialog(Gtk.Dialog):
payload.append(0)
self.username_entry.set_text("")
self.password_entry.set_text("")
self.password_confirm_entry.set_text("")
self.two_factor_entry.set_text("")
del username, password, password_confirm, two_factor
del username, password, two_factor
self.begin_busy_state()
threading.Thread(target=self.account_switch_worker, args=(payload,), daemon=True).start()

Expand Down
4 changes: 2 additions & 2 deletions docs/OPERATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,7 @@ only its own marked autostart file. A manual menu launch remains visible.

The Preferences **Account** section is a guarded action rather than a saved UI
preference. **Change Proton account …** accepts a new Proton username or email,
password, repeated password and an optional fresh six-digit 2FA code. It has no
password and an optional fresh six-digit 2FA code. It has no
default credential value and stores none of those inputs in the UI preference
file. Activation occurs only after isolated authentication, a repeated clean
preflight and validated remounting. Its persistent effects are the encrypted
Expand Down Expand Up @@ -716,7 +716,7 @@ Reauthorization preserves the existing account identity and VFS namespace;
`pdrive-account-switch --switch` deliberately changes both. The Control Center
access path is **hamburger menu → Preferences → Account → Change Proton account
…**. Its full native form accepts the candidate username or email, password,
repeated password and optional fresh six-digit 2FA code. The explicit checkbox
and optional fresh six-digit 2FA code. The explicit checkbox
confirms that `/pdrive` and the Proton remote namespace will change while old
local cache data remains separate.

Expand Down
Binary file modified docs/assets/pdrive-account-switch.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 0 additions & 9 deletions libexec/setup-rclone-proton
Original file line number Diff line number Diff line change
Expand Up @@ -96,15 +96,6 @@ else
read -r -p 'Proton username or email address: ' username
read -r -s -p 'Proton password: ' password
printf '\n'
read -r -s -p 'Repeat password: ' password_confirm
printf '\n'
if [[ -z "${password}" || "${password}" != "${password_confirm}" ]]; then
unset password password_confirm
printf 'Passwords are empty or do not match.\n' >&2
exit 65
fi
unset password_confirm

read -r -s -p 'Current six-digit 2FA code (empty if 2FA is disabled): ' two_factor
printf '\n'
fi
Expand Down
9 changes: 7 additions & 2 deletions tests/test-setup-wizard-ui.sh
Original file line number Diff line number Diff line change
Expand Up @@ -155,15 +155,20 @@ bandwidth_log = pathlib.Path(module.os.environ["PDRIVE_TEST_BANDWIDTH_LOG"])
assert bandwidth_log.read_text(encoding="utf-8").splitlines() == ["4.80:12.00"]
wizard.bandwidth_continue_button.emit("clicked")
assert wizard.stack.get_visible_child_name() == "account"
assert len(
[
widget
for widget in wizard.password_entry.get_parent().get_children()
if isinstance(widget, module.Gtk.Entry)
]
) == 3

def submit():
wizard.username_entry.set_text("person@example.test")
wizard.password_entry.set_text("correct horse battery staple")
wizard.password_confirm_entry.set_text("correct horse battery staple")
wizard.two_factor_entry.set_text("123456")
wizard.on_connect(module.Gtk.Button())
assert wizard.password_entry.get_text() == ""
assert wizard.password_confirm_entry.get_text() == ""
assert wizard.two_factor_entry.get_text() == ""

module.os.environ["PDRIVE_TEST_SETUP_FAIL"] = "1"
Expand Down
5 changes: 3 additions & 2 deletions tests/test-ui-widgets.sh
Original file line number Diff line number Diff line change
Expand Up @@ -616,17 +616,17 @@ while module.Gtk.events_pending():
assert account_dialog.get_title() == "Change Proton account"
assert account_dialog.preflight_ready
assert "no active transfers" in account_dialog.preflight_label.get_text()
assert account_dialog.show_password.get_label() == "Show password and 2FA code"
assert len(
[
widget
for widget in descendants(account_dialog.form)
if isinstance(widget, module.Gtk.Entry)
]
) == 4
) == 3
assert not account_dialog.switch_button.get_sensitive()
account_dialog.username_entry.set_text("candidate-user")
account_dialog.password_entry.set_text("generated-test-password")
account_dialog.password_confirm_entry.set_text("generated-test-password")
account_dialog.two_factor_entry.set_text("123")
account_dialog.confirmation.set_active(True)
assert not account_dialog.switch_button.get_sensitive()
Expand Down Expand Up @@ -658,6 +658,7 @@ assert german_account_dialog.get_title() == "Proton-Konto wechseln"
assert german_account_dialog.hero_title.get_text() == "/pdrive zu einem anderen Konto verschieben"
assert german_account_dialog.switch_button.get_label() == "Konto sicher wechseln"
assert german_account_dialog.confirmation.get_label().startswith("Ich verstehe")
assert german_account_dialog.show_password.get_label() == "Passwort und 2FA-Code anzeigen"
german_account_dialog.destroy()
module.CURRENT_LANGUAGE = "en"

Expand Down