rename files on SD card via List Files
What changed, and why it matters
This commit adds a small new feature to the COLDCARD hardware wallet: users can now rename files stored on the device's SD card through the on-device menu. The change is a user-experience enhancement, not a security fix. It includes basic input validation to block slashes and spaces in filenames, and adds automated tests for the new behavior.
No immediate security action required. As a defensive review, verify that the character blacklist is sufficient to prevent path traversal or unintended overwrites on the target filesystem, and consider whether additional validation (e.g., reserved names, length limits, or destination existence checks) is warranted before release.
Security signals we found
New file-system operation exposed through on-device UI
Input validation added for slash, backslash, and space characters in filenames
No path traversal or directory escape validation beyond character blacklist
No mention of security bug, CVE, or researcher attribution in commit or references
Evidence from the diff
The patch extends the existing list_files() UX flow in shared/actions.py to support renaming a selected SD card file. It adds a new keypress option ‘(1) to rename file’, calls ux_input_text() for the new basename, rejects filenames containing /, \, or space via an assertion, and uses uos.rename() to perform the rename. The changelog labels this as an enhancement, and new tests in testing/test_ux.py verify both successful rename and rejection of illegal characters.
Changed components
shared/actions.py: list_files() UX flowtesting/test_ux.py: new test_rename_from_list_files testreleases/Next-ChangeLog.mdInspect captured patch +84 / −13
diff --git a/releases/Next-ChangeLog.md b/releases/Next-ChangeLog.md
index ecb7296..b5ea118 100644
--- a/releases/Next-ChangeLog.md
+++ b/releases/Next-ChangeLog.md
@@ -8,6 +8,7 @@ This lists the new changes that have not yet been published in a normal release.
- Enhancement: Add warning for zero value outputs if not `OP_RETURN`
- Enhancement: Show QR codes of output addresses in transaction output explorer. Explorer is
now offered for transactions of all sizes, not just complex ones.
+ Enhancement: Add ability to rename files on SD card via `Advanced/Tools -> File Management -> List Files`
- Bugfix: If all change outputs have `nValue=0` they were not shown in UX.
- Bugfix: Disallow negative input/output amounts in PSBT.
- Bugfix: Fix filesystem initialization after Wife LFS or Destroy Seed.
diff --git a/shared/actions.py b/shared/actions.py
index f64fe18..7701cb5 100644
--- a/shared/actions.py
+++ b/shared/actions.py
@@ -1683,30 +1683,39 @@ async def list_files(*A):
from pincodes import pa
digest = chk.digest()
- basename = fn.rsplit('/', 1)[-1]
- msg_base = 'SHA256(%s)\n\n%s\n\nPress ' % (basename, B2A(digest))
- escape = "6"
+ path, basename = fn.rsplit('/', 1)
+ msg_base = 'SHA256(%s)\n\n' + B2A(digest) + '\n\nPress (1) to rename file, '
+ escape = "61"
if pa.has_secrets():
- msg_sign = '(4) to sign file digest and export detached signature, '
+ msg_base += '(4) to sign file digest and export detached signature, '
escape += "4"
- else:
- msg_sign = ""
- msg_delete = '(6) to delete.'
- msg = msg_base + msg_sign + msg_delete
+ msg_base += '(6) to delete.'
+
while True:
- ch = await ux_show_story(msg, escape=escape)
+ ch = await ux_show_story(msg_base % basename, escape=escape)
if ch == "x": break
- if ch in '46':
+ if ch in '461':
with CardSlot() as card:
if ch == '6':
card.securely_blank_file(fn)
break
+ elif ch == '1':
+ new_basename = await ux_input_text(basename, max_len=32, min_len=3)
+ if new_basename:
+ try:
+ # prohibit both slashes and space in filenames
+ for s in "\/ ":
+ assert s not in new_basename, "illegal char"
+ uos.rename(path + "/" + basename, path + "/" + new_basename)
+ basename = new_basename
+ except Exception as e:
+ await ux_show_story("Failed to rename the file. " + str(e),
+ title="Failure")
else:
from msgsign import write_sig_file
sig_nice = write_sig_file([(digest, fn)])
await ux_show_story("Signature file %s written." % sig_nice)
- msg = msg_base + msg_delete
return
async def file_picker(suffix=None, min_size=1, max_size=1000000, taster=None,
diff --git a/testing/test_ux.py b/testing/test_ux.py
index e0cfcc7..afb1eea 100644
--- a/testing/test_ux.py
+++ b/testing/test_ux.py
@@ -2,7 +2,7 @@
#
import pytest, time, os, re, hashlib, shutil
from helpers import xfp2str, prandom
-from charcodes import KEY_DOWN, KEY_QR, KEY_NFC, KEY_DELETE
+from charcodes import KEY_DOWN, KEY_QR, KEY_NFC, KEY_DELETE, KEY_CANCEL
from constants import AF_CLASSIC, simulator_fixed_words, simulator_fixed_xfp
from mnemonic import Mnemonic
from bip32 import BIP32Node
@@ -818,7 +818,6 @@ def test_sign_file_from_list_files(f_len, goto_home, cap_story, pick_menu_item,
verify_detached_signature_file([fname], signame, "sd", AF_CLASSIC)
time.sleep(0.1)
_, story = cap_story()
- assert "(4) to sign file digest and export detached signature" not in story
assert "(6) to delete" in story
@@ -828,6 +827,68 @@ def test_sign_file_from_list_files(f_len, goto_home, cap_story, pick_menu_item,
assert "List Files" in menu
+def test_rename_from_list_files(goto_home, cap_story, pick_menu_item, need_keypress, is_q1,
+ microsd_path, press_select, cap_screen, enter_complex):
+ def clear(fname):
+ for i in range(len(fname)):
+ if not is_q1 and not i:
+ # Mk4 different menu entry UX
+ continue
+ need_keypress(KEY_DELETE if is_q1 else "x")
+ time.sleep(0.01)
+
+ fname = "file_to_rename.pdf"
+ fpath = microsd_path(fname)
+ contents = os.urandom(64)
+ digest = hashlib.sha256(contents).digest().hex()
+ with open(fpath, "wb") as f:
+ f.write(contents)
+
+ goto_home()
+ pick_menu_item("Advanced/Tools")
+ pick_menu_item('File Management')
+ pick_menu_item('List Files')
+ time.sleep(0.1)
+ pick_menu_item(fname)
+ time.sleep(0.1)
+ _, story = cap_story()
+ assert f"SHA256({fname})" in story
+ assert digest in story
+ assert "Press (1) to rename file" in story
+ need_keypress("1")
+ time.sleep(0.1)
+ if is_q1:
+ scr = cap_screen()
+ assert fname in scr
+
+ clear(fname)
+
+ bad_fnames = ["renamed file.txt", "/sd/renamed_file.txt", "renamed\\file.txt"]
+ for bad in bad_fnames:
+ enter_complex(bad, b39pass=False)
+ time.sleep(.1)
+ title, story = cap_story()
+ assert title == "Failure"
+ assert "Failed to rename the file" in story
+ assert "illegal char" in story
+ press_select()
+ time.sleep(.1)
+ need_keypress("1") # rename again
+ time.sleep(.1)
+ clear(fname)
+ if not is_q1:
+ need_keypress("1") # toggle case back to upper (enter complex expect to start in that state)
+
+ new_fname = "renamed_file.txt"
+ enter_complex(new_fname, b39pass=False)
+ time.sleep(.1)
+ _, story = cap_story()
+ assert f"SHA256({new_fname})" in story
+ assert digest in story
+ assert not os.path.exists(fpath)
+ assert os.path.exists(microsd_path(new_fname))
+
+
def test_bip39_pw_signing_xfp_ux(pick_menu_item, press_select, cap_story, enter_complex,
reset_seed_words, cap_menu, go_to_passphrase, microsd_wipe):
microsd_wipe() # need to wipe all PSBT on SD card so we do not proceed to signing
Why this scored 22/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.