Compare commits

...
6 Commits
Author SHA1 Message Date
besendorf df540d7c41 Merge pull request #984 from arxhr007/feat/979-reprompt-backup-password
Ask for the backup password again when it is wrong
2026-10-28 09:30:52 +01:00
arxhr007 706e81dff2 feat(android): ask for the backup password again when it is wrong
A wrong password typed at the prompt ended the check with "Invalid backup
password", and check-androidqf skipped the backup modules. An analyst who
was handed several candidate passwords had to start the whole run again
for each one.

Ask for the password again, up to three attempts in total. A password
given with --backup-password or in the environment is not asked for
again, as before, so scripted runs fail the same way.

Fixes #979
2026-10-27 21:43:01 +05:30
github-actions[bot] 3ba15716a2 Add new iOS versions and build numbers (#983) 2026-10-27 08:07:27 +01:00
8068569dce Hide the per-command --verbose aliases from --help (#965)
The group-level -v/--verbose on mvt-ios and mvt-android is the documented
form; the per-command flags stay as hidden aliases for compatibility.


Claude-Session: https://claude.ai/code/session_01FiSwVbasf7HbjC18fALafb

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Donncha Ó Cearbhaill <donncha.ocearbhaill@amnesty.org>
2026-10-26 15:03:01 +01:00
CHE3MZandDonncha Ó Cearbhaill 6e9408e30c Add favicon and logo to mkdocs site and add dark/light mode toggle (#966)
* Add favicon and logo to mkdocs site and add dark/light mode toggle

* Use black primary colour and follow the OS light/dark mode

Switch the palette to the automatic light/dark mode setup from the
Material for MkDocs docs, with the same toggle icons the Material site
uses. Drop the blue grey accent, which Material does not support as an
accent colour, and remove trailing whitespace.

---------

Co-authored-by: Donncha Ó Cearbhaill <donncha.ocearbhaill@amnesty.org>
2026-10-26 13:20:34 +01:00
scribblemaniac db809cbc46 Update dockerfiles to revoke eMudhra intermediate certs (#981) 2026-10-26 12:25:47 +01:00
11 changed files with 163 additions and 34 deletions

No files matched your search

+2 -2
View File
@@ -1,6 +1,6 @@
# Create main image
FROM ubuntu:24.04 AS main
COPY --from=ghcr.io/astral-sh/uv:0.11.8 /uv /uvx /usr/local/bin/
COPY --from=ghcr.io/astral-sh/uv:0.13.5 /uv /uvx /usr/local/bin/
LABEL org.opencontainers.image.url="https://mvt.re"
LABEL org.opencontainers.image.documentation="https://docs.mvt.re"
@@ -62,4 +62,4 @@ WORKDIR /home/cases
RUN echo 'echo "Mobile Verification Toolkit @ Docker\n------------------------------------\n\nYou can find information about how to use this image for Android (https://github.com/mvt-project/mvt/tree/master/docs/android) and iOS (https://github.com/mvt-project/mvt/tree/master/docs/ios) in the official docs of the project.\n"' >> ~/.bashrc \
&& echo 'echo "Note that to perform the debug via USB you might need to give the Docker image access to the USB using \"docker run -it --privileged -v /dev/bus/usb:/dev/bus/usb mvt\" or, preferably, the \"--device=\" parameter.\n"' >> ~/.bashrc
CMD /bin/bash
CMD [ "/bin/bash" ]
+3 -3
View File
@@ -1,6 +1,6 @@
# Create main image
FROM python:3.10.14-alpine3.20 AS main
COPY --from=ghcr.io/astral-sh/uv:0.11.8 /uv /uvx /usr/local/bin/
FROM python:3.11.17-alpine3.23 AS main
COPY --from=ghcr.io/astral-sh/uv:0.13.5 /uv /uvx /usr/local/bin/
LABEL org.opencontainers.image.url="https://mvt.re"
LABEL org.opencontainers.image.documentation="https://docs.mvt.re"
@@ -8,7 +8,7 @@ LABEL org.opencontainers.image.source="https://github.com/mvt-project/mvt"
LABEL org.opencontainers.image.title="Mobile Verification Toolkit (Android)"
LABEL org.opencontainers.image.description="MVT is a forensic tool to look for signs of infection in smartphone devices."
LABEL org.opencontainers.image.licenses="MVT License 1.1"
LABEL org.opencontainers.image.base.name=docker.io/library/python:3.10.14-alpine3.20
LABEL org.opencontainers.image.base.name=docker.io/library/python:3.11.17-alpine3.23
# Install runtime dependencies
RUN apk add --no-cache \
+1 -1
View File
@@ -1,7 +1,7 @@
# Main image
# ----------
FROM ubuntu:24.04 AS main
COPY --from=ghcr.io/astral-sh/uv:0.11.8 /uv /uvx /usr/local/bin/
COPY --from=ghcr.io/astral-sh/uv:0.13.5 /uv /uvx /usr/local/bin/
LABEL org.opencontainers.image.url="https://mvt.re"
LABEL org.opencontainers.image.documentation="https://docs.mvt.re"
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

+20
View File
@@ -18,8 +18,28 @@ markdown_extensions:
use_pygments: true
theme:
name: material
logo: mvt.png
favicon: favicon.png
features:
- tabs
palette:
- media: "(prefers-color-scheme)"
primary: black
toggle:
icon: material/link
name: Switch to light mode
- media: "(prefers-color-scheme: light)"
scheme: default
primary: black
toggle:
icon: material/toggle-switch
name: Switch to dark mode
- media: "(prefers-color-scheme: dark)"
scheme: slate
primary: black
toggle:
icon: material/toggle-switch-off
name: Switch to system preference
plugins:
- search
- mkdocstrings
+12 -4
View File
@@ -160,7 +160,9 @@ def check_adb(ctx):
"zone is read from persist.sys.timezone in the bugreport itself."
),
)
@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE_COMMAND)
@click.option(
"--verbose", "-v", is_flag=True, hidden=True, help=HELP_MSG_VERBOSE_COMMAND
)
@click.argument("BUGREPORT_PATH", type=click.Path(exists=True))
@click.pass_context
def check_bugreport(
@@ -235,7 +237,9 @@ def check_bugreport(
)
@click.option("--non-interactive", "-n", is_flag=True, help=HELP_MSG_NONINTERACTIVE)
@click.option("--backup-password", "-p", help=HELP_MSG_ANDROID_BACKUP_PASSWORD)
@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE_COMMAND)
@click.option(
"--verbose", "-v", is_flag=True, hidden=True, help=HELP_MSG_VERBOSE_COMMAND
)
@click.argument("BACKUP_PATH", type=click.Path(exists=True))
@click.pass_context
def check_backup(
@@ -310,7 +314,9 @@ def check_backup(
)
@click.option("--non-interactive", "-n", is_flag=True, help=HELP_MSG_NONINTERACTIVE)
@click.option("--backup-password", "-p", help=HELP_MSG_ANDROID_BACKUP_PASSWORD)
@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE_COMMAND)
@click.option(
"--verbose", "-v", is_flag=True, hidden=True, help=HELP_MSG_VERBOSE_COMMAND
)
@click.argument("ANDROIDQF_PATH", type=click.Path(exists=True))
@click.pass_context
def check_androidqf(
@@ -397,7 +403,9 @@ def check_androidqf(
"time instead of UTC."
),
)
@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE_COMMAND)
@click.option(
"--verbose", "-v", is_flag=True, hidden=True, help=HELP_MSG_VERBOSE_COMMAND
)
@click.argument("LOGS_PATH", type=click.Path(exists=True))
@click.pass_context
def check_intrusion_logs(
+42 -19
View File
@@ -27,6 +27,8 @@ from .modules.backup import BACKUP_MODULES
log = logging.getLogger(__name__)
MAX_BACKUP_PASSWORD_ATTEMPTS = 3
class InvalidAndroidBackup(Exception):
pass
@@ -72,6 +74,15 @@ class CmdAndroidCheckBackup(Command):
self.__tar: Optional[tarfile.TarFile] = None
self.__files: List[str] = []
def _get_backup_password(self) -> str:
password = prompt_or_load_android_backup_password(log, self.module_options)
if not password:
if self.sub_command:
raise InvalidAndroidBackup("No backup password provided")
log.critical("No backup password provided.")
sys.exit(1)
return password
def from_ab(self, ab_file_bytes: bytes) -> None:
self.__type = "ab"
header = parse_ab_header(ab_file_bytes)
@@ -84,28 +95,40 @@ class CmdAndroidCheckBackup(Command):
sys.exit(1)
password = None
# A password typed at the prompt is asked for again if it is wrong.
# One given on the command line or in the environment is not.
max_attempts = 1
if header["encryption"] != "none":
password = prompt_or_load_android_backup_password(log, self.module_options)
if not password:
if not self.module_options.get("backup_password", None):
max_attempts = MAX_BACKUP_PASSWORD_ATTEMPTS
password = self._get_backup_password()
attempt = 1
while True:
try:
tardata = parse_backup_file(ab_file_bytes, password=password)
break
except InvalidBackupPassword:
if attempt < max_attempts:
attempt += 1
log.error(
"Invalid backup password, try again (attempt %d of %d)",
attempt,
max_attempts,
)
password = self._get_backup_password()
continue
if self.sub_command:
raise InvalidAndroidBackup("No backup password provided")
log.critical("No backup password provided.")
raise InvalidAndroidBackup("Invalid backup password")
log.critical("Invalid backup password")
sys.exit(1)
except AndroidBackupParsingError as exc:
if self.sub_command:
raise InvalidAndroidBackup(
f"Impossible to parse this backup file: {exc}"
) from exc
log.critical("Impossible to parse this backup file: %s", exc)
log.critical("Please use Android Backup Extractor (ABE) instead")
sys.exit(1)
try:
tardata = parse_backup_file(ab_file_bytes, password=password)
except InvalidBackupPassword:
if self.sub_command:
raise InvalidAndroidBackup("Invalid backup password")
log.critical("Invalid backup password")
sys.exit(1)
except AndroidBackupParsingError as exc:
if self.sub_command:
raise InvalidAndroidBackup(
f"Impossible to parse this backup file: {exc}"
) from exc
log.critical("Impossible to parse this backup file: %s", exc)
log.critical("Please use Android Backup Extractor (ABE) instead")
sys.exit(1)
dbytes = io.BytesIO(tardata)
try:
+9 -3
View File
@@ -278,7 +278,9 @@ def extract_key(password, key_file, backup_path):
help=HELP_MSG_LOAD_MODULE,
)
@click.option("--hashes", "-H", is_flag=True, help=HELP_MSG_HASHES)
@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE_COMMAND)
@click.option(
"--verbose", "-v", is_flag=True, hidden=True, help=HELP_MSG_VERBOSE_COMMAND
)
@click.argument("BACKUP_PATH", type=click.Path(exists=True))
@click.pass_context
def check_backup(
@@ -347,7 +349,9 @@ def check_backup(
help=HELP_MSG_LOAD_MODULE,
)
@click.option("--hashes", "-H", is_flag=True, help=HELP_MSG_HASHES)
@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE_COMMAND)
@click.option(
"--verbose", "-v", is_flag=True, hidden=True, help=HELP_MSG_VERBOSE_COMMAND
)
@click.argument("DUMP_PATH", type=click.Path(exists=True))
@click.pass_context
def check_fs(
@@ -416,7 +420,9 @@ def check_fs(
help=HELP_MSG_LOAD_MODULE,
)
@click.option("--hashes", "-H", is_flag=True, help=HELP_MSG_HASHES)
@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE_COMMAND)
@click.option(
"--verbose", "-v", is_flag=True, hidden=True, help=HELP_MSG_VERBOSE_COMMAND
)
@click.argument("SYSDIAGNOSE_PATH", type=click.Path(exists=True))
@click.pass_context
def check_sysdiagnose(
+12
View File
@@ -1224,6 +1224,10 @@
"version": "18.7.10",
"build": "22H374"
},
{
"version": "18.7.11",
"build": "22H384"
},
{
"version": "26",
"build": "23A341"
@@ -1283,5 +1287,13 @@
{
"version": "27.0.1",
"build": "24A446"
},
{
"version": "27.2",
"build": "24B91020"
},
{
"version": "27.2",
"build": "24B119"
}
]
+60
View File
@@ -59,6 +59,66 @@ class TestCheckAndroidBackupCommand:
assert result.exit_code == 1
assert "Invalid backup password" in caplog.text
def test_check_encrypted_backup_prompt_retry(self, mocker):
"""A wrong password at the prompt is asked for again"""
prompt_mock = mocker.patch(
"mvt.android.modules.backup.helpers.prompt_password",
side_effect=["wrong_password", TEST_BACKUP_PASSWORD],
)
runner = CliRunner()
path = os.path.join(get_artifact_folder(), "androidqf_encrypted/backup.ab")
result = runner.invoke(check_backup, [path])
assert prompt_mock.call_count == 2
assert result.exit_code == 0
def test_check_encrypted_backup_prompt_gives_up(self, mocker, caplog):
"""The prompt gives up after three wrong passwords"""
prompt_mock = mocker.patch(
"mvt.android.modules.backup.helpers.prompt_password",
return_value="wrong_password",
)
runner = CliRunner()
path = os.path.join(get_artifact_folder(), "androidqf_encrypted/backup.ab")
with caplog.at_level(logging.CRITICAL):
result = runner.invoke(check_backup, [path])
assert prompt_mock.call_count == 3
assert result.exit_code == 1
assert "Invalid backup password" in caplog.text
def test_check_encrypted_backup_prompt_retry_empty(self, mocker, caplog):
"""An empty answer on a retry ends the check"""
prompt_mock = mocker.patch(
"mvt.android.modules.backup.helpers.prompt_password",
side_effect=["wrong_password", ""],
)
runner = CliRunner()
path = os.path.join(get_artifact_folder(), "androidqf_encrypted/backup.ab")
with caplog.at_level(logging.CRITICAL):
result = runner.invoke(check_backup, [path])
assert prompt_mock.call_count == 2
assert result.exit_code == 1
assert "No backup password provided" in caplog.text
def test_check_encrypted_backup_cli_invalid_no_prompt(self, mocker):
"""A password given on the command line is not asked for again"""
prompt_mock = mocker.patch(
"mvt.android.modules.backup.helpers.prompt_password",
return_value=TEST_BACKUP_PASSWORD,
)
runner = CliRunner()
path = os.path.join(get_artifact_folder(), "androidqf_encrypted/backup.ab")
result = runner.invoke(
check_backup, ["--backup-password", "invalid_password", path]
)
assert prompt_mock.call_count == 0
assert result.exit_code == 1
def test_check_encrypted_backup_env(self, mocker):
"""Provide password as environment variable"""
prompt_mock = mocker.patch(
+2 -2
View File
@@ -100,8 +100,8 @@ class TestVerboseOnTheCheckCommands:
assert result.exit_code == 0
assert _console_level() == logging.DEBUG
def test_the_command_option_says_it_is_kept_for_compatibility(self):
def test_the_command_option_is_hidden_from_the_help(self):
result = CliRunner().invoke(ios_cli, [*OFFLINE, "check-backup", "--help"])
assert result.exit_code == 0
assert "kept for compatibility" in result.output
assert "--verbose" not in result.output