Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sms mfa using str instead of int to keep leading zeros #993 #995

Merged
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: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

- fix: sms MFA dropping leading zeros [#993](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/993)

## 1.24.3 (2024-11-03)

- fix: crashes when no imagetype sent by Apple [ref](https://github.com/boredazfcuk/docker-icloudpd/issues/680)
Expand Down
34 changes: 21 additions & 13 deletions src/icloudpd/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ def request_2sa(icloud: PyiCloudService, logger: logging.Logger) -> None:
number = device["phoneNumber"]
alt_name = f"SMS to {number}"
name = device.get("deviceName", alt_name)
print(f" {i}: {name}")
click.echo(f" {i}: {name}")

device_index = click.prompt(
"Please choose an option:", default="0", type=click.IntRange(0, devices_count - 1)
Expand Down Expand Up @@ -138,7 +138,7 @@ def request_2fa(icloud: PyiCloudService, logger: logging.Logger) -> None:
sys.exit(1)

for i, device in enumerate(devices):
print(f" {device_index_alphabet[i]}: {device.obfuscated_number}")
click.echo(f" {device_index_alphabet[i]}: {device.obfuscated_number}")

index_str = f"..{device_index_alphabet[devices_count - 1]}" if devices_count > 1 else ""
index_or_code: str = ""
Expand All @@ -159,14 +159,14 @@ def request_2fa(icloud: PyiCloudService, logger: logging.Logger) -> None:
if index_or_code in device_index_alphabet:
if device_index_alphabet.index(index_or_code) > devices_count - 1:
click.echo(
f"Invalid index, should be {device_index_alphabet[0]}{index_str}. Try again"
f"Invalid index, should be ({device_index_alphabet[0]}{index_str}). Try again",
)
continue
else:
break
else:
click.echo(
f"Invalid index, should be {device_index_alphabet[0]}{index_str}. Try again"
f"Invalid index, should be ({device_index_alphabet[0]}{index_str}). Try again",
)
continue

Expand All @@ -178,7 +178,7 @@ def request_2fa(icloud: PyiCloudService, logger: logging.Logger) -> None:
continue

click.echo(
f"Should be index {device_index_alphabet[0]}{index_str} or six-digit code. Try again"
f"Should be index ({device_index_alphabet[0]}{index_str}) or six-digit code. Try again",
)

if index_or_code in device_index_alphabet:
Expand All @@ -188,10 +188,14 @@ def request_2fa(icloud: PyiCloudService, logger: logging.Logger) -> None:
if not icloud.send_2fa_code_sms(device.id):
logger.error("Failed to send two-factor authentication code")
sys.exit(1)
code: int = click.prompt(
"Please enter two-factor authentication code that you received over SMS",
type=click.IntRange(0, 999999),
)
while True:
code: str = click.prompt(
"Please enter two-factor authentication code that you received over SMS",
).strip()
if len(code) == 6 and code.isdigit():
break
click.echo("Invalid code, should be six digits. Try again")

if not icloud.validate_2fa_code_sms(device.id, code):
logger.error("Failed to verify two-factor authentication code")
sys.exit(1)
Expand All @@ -200,10 +204,14 @@ def request_2fa(icloud: PyiCloudService, logger: logging.Logger) -> None:
logger.error("Failed to verify two-factor authentication code")
sys.exit(1)
else:
code = click.prompt(
"Please enter two-factor authentication code", type=click.IntRange(0, 999999)
)
if not icloud.validate_2fa_code(str(code)):
while True:
code = click.prompt(
"Please enter two-factor authentication code",
).strip()
if len(code) == 6 and code.isdigit():
break
click.echo("Invalid code, should be six digits. Try again")
if not icloud.validate_2fa_code(code):
logger.error("Failed to verify two-factor authentication code")
sys.exit(1)
logger.info(
Expand Down
2 changes: 1 addition & 1 deletion src/pyicloud_ipd/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,7 @@ def validate_verification_code(self, device: Dict[str, Any], code: str) -> bool:

return not self.requires_2sa

def validate_2fa_code_sms(self, device_id: int, code:int) -> bool:
def validate_2fa_code_sms(self, device_id: int, code:str) -> bool:
"""Verifies a verification code received via Apple's 2FA system through SMS."""

oauth_session = self.get_oauth_session()
Expand Down
4 changes: 2 additions & 2 deletions src/pyicloud_ipd/sms.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,12 +151,12 @@ def build_send_sms_code_request(context: _TrustedPhoneContextProvider, device_id
json = json)
return req

def build_verify_sms_code_request(context: _TrustedPhoneContextProvider, device_id: int, code: int) -> Request:
def build_verify_sms_code_request(context: _TrustedPhoneContextProvider, device_id: int, code: str) -> Request:
""" Builds a request for the list of trusted phone numbers for sms 2fa """

url = _auth_url(context.domain) + "/verify/phone/securitycode"

json = {"phoneNumber":{"id":device_id},"securityCode":{"code":str(code)},"mode":"sms"}
json = {"phoneNumber":{"id":device_id},"securityCode":{"code":code},"mode":"sms"}

req = _InternalRequest(
method="POST",
Expand Down
39 changes: 39 additions & 0 deletions tests/test_two_step_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,3 +195,42 @@ def test_2fa_flow_valid_code(self) -> None:
self._caplog.text,
)
assert result.exit_code == 0

def test_2fa_flow_valid_code_zero_lead(self) -> None:
base_dir = os.path.join(self.fixtures_path, inspect.stack()[0][3])
cookie_dir = os.path.join(base_dir, "cookie")

for dir in [base_dir, cookie_dir]:
recreate_path(dir)

with vcr.use_cassette(os.path.join(self.vcr_path, "2fa_flow_valid_code_zero_lead.yml")):
runner = CliRunner(env={"CLIENT_ID": "DE309E26-942E-11E8-92F5-14109FE0B321"})
result = runner.invoke(
main,
[
"--username",
"[email protected]",
"--password",
"password1",
"--no-progress-bar",
"--cookie-directory",
cookie_dir,
"--auth-only",
],
input="054321\n",
)
self.assertIn("DEBUG Authenticating...", self._caplog.text)
self.assertIn(
"INFO Two-factor authentication is required",
self._caplog.text,
)
self.assertIn(
"Please enter two-factor authentication code or device index (a) to send SMS with a code: 054321",
result.output,
)
self.assertIn(
"INFO Great, you're all set up. The script can now be run without "
"user interaction until 2FA expires.",
self._caplog.text,
)
assert result.exit_code == 0
Loading
Loading