Skip to content
Open
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
10 changes: 9 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ ifneq ($(ROMFS),)
export _3DSXFLAGS += --romfs=$(CURDIR)/$(ROMFS)
endif

.PHONY: $(BUILD) clean all ime-dict test-ime cia cia-tools cia-clean
.PHONY: $(BUILD) clean all ime-dict test-ime test-config test-terminal cia cia-tools cia-clean

all: $(BUILD)

Expand All @@ -118,6 +118,14 @@ ime-dict:
test-ime:
@bash tools/test_ime.sh

# Host-side config parser tests (including quoted keychain passwords).
test-config:
@bash tools/test_config.sh

# Host-side terminal protocol tests (scrolling + fish cursor queries).
test-terminal:
@bash tools/test_terminal.sh

# ── M9: CIA packaging ───────────────────────────────────────────────
#
# `make cia` produces DSSH.cia from the already-built ELF + romfs.
Expand Down
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,9 @@ port = 22
user = ubuntu
key_path = sdmc:/3ds/3dssh/id_rsa
passphrase =

# Optional: unlock the current SSH user's macOS login keychain
macos_keychain_password =
```

| Field | Meaning |
Expand All @@ -192,6 +195,32 @@ passphrase =
| `user` | SSH login user |
| `key_path` | Private key path; `sdmc:/...` is the 3DS standard SD prefix |
| `passphrase` | Optional key passphrase; leave empty (typing one on the soft keyboard is awkward) |
| `macos_keychain_password` | Optional macOS login password; setting it enables automatic unlock for the current SSH user |

When `macos_keychain_password` is set, DSSH waits for the current interactive PTY
shell to finish initialization, then runs
`/usr/bin/security unlock-keychain "$HOME/Library/Keychains/login.keychain-db"`.
It waits until macOS prints `password to unlock` before sending the password
through the PTY—not `-p`, the remote process arguments, or shell history. This
matches macOS Keychain's interactive-session requirement and works with fish,
zsh, and bash. DSSH then clears its in-memory copy. If unlock fails, the normal
SSH shell remains available and a warning is shown. A later `claude` launch can
then read the credential already stored in that login keychain.

Successful unlock diagnostics are hidden and the bootstrap output is cleared
before fish redraws its prompt. On failure, DSSH keeps one concise warning with
the unlock and verification status codes. The password itself is never shown.

Quote a password containing `#` or leading/trailing spaces:

```ini
macos_keychain_password = "my # password"
```

> ⚠️ This option stores your **macOS login password in plaintext on the
> removable SD card**. That is more sensitive than a dedicated SSH key. Use a
> dedicated macOS account where practical, keep the SD card physically secure,
> and leave this field empty unless you accept this risk.

Final SD layout:

Expand Down
27 changes: 27 additions & 0 deletions README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,9 @@ port = 22
user = ubuntu
key_path = sdmc:/3ds/3dssh/id_rsa
passphrase =

# 可选:自动解锁当前 SSH 用户的 macOS 登录 keychain
macos_keychain_password =
```

| 字段 | 说明 |
Expand All @@ -175,6 +178,30 @@ passphrase =
| `user` | 服务器登录用户名 |
| `key_path` | 私钥路径,`sdmc:/...` 是 3DS 标准 SD 路径前缀 |
| `passphrase` | 私钥口令;建议留空(SD 卡上输 passphrase 体验差) |
| `macos_keychain_password` | 可选的 macOS 登录密码;填写后为当前 SSH 用户启用自动解锁 |

填写 `macos_keychain_password` 后,DSSH 会等待当前交互式 PTY shell 初始化完成,
然后执行
`/usr/bin/security unlock-keychain "$HOME/Library/Keychains/login.keychain-db"`。
只有检测到 macOS 输出 `password to unlock` 后才会通过 PTY 发送密码;不使用
`-p`,不会进入远端进程参数或 shell history。这满足 macOS Keychain 对交互式
会话的要求,并兼容 fish、zsh 和 bash。之后 DSSH 会清除内存中的密码副本。
即使解锁失败,普通 SSH shell 仍可正常使用,并会显示一条警告。之后再启动
`claude`,它就可以读取登录 keychain 中已有的 credential。

解锁成功时不会再显示诊断日志,DSSH 会清理 bootstrap 输出后再由 fish
重绘提示符。失败时只保留一条包含 unlock/verify 状态码的简短警告;密码
本身永远不会显示。

如果密码包含 `#` 或首尾空格,请加引号:

```ini
macos_keychain_password = "my # password"
```

> ⚠️ 此功能会把你的 **macOS 登录密码以明文保存在可移除的 SD 卡上**,其
> 敏感程度高于一把专用 SSH 密钥。建议尽量使用专门的 macOS 账户,妥善保管
> SD 卡;只有接受这一风险时才填写该字段。

最终 SD 卡布局:

Expand Down
11 changes: 11 additions & 0 deletions sd_template/3ds/3dssh/config.ini.example
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,14 @@ key_path = sdmc:/3ds/3dssh/id_rsa
# Passphrase for the key, or leave empty for an unencrypted key.
# (Recommended: unencrypted, since the SD card is the only place the key lives.)
passphrase =

# Optional: automatically unlock the current SSH user's macOS login keychain.
# Leave empty to disable it.
# The password is sent only after the interactive PTY shows its unlock prompt;
# it is never placed in a command-line argument or shell history.
# Quote it if it contains # or leading/trailing spaces, for example:
# macos_keychain_password = "my # password"
#
# SECURITY WARNING: this stores your macOS login password in plaintext on the
# removable SD card. Prefer a dedicated macOS account and keep the SD card safe.
macos_keychain_password =
50 changes: 48 additions & 2 deletions source/config.c
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,49 @@ static void trim(char *s) {
while (len > 0 && isspace((unsigned char)s[len - 1])) s[--len] = 0;
}

/* Remove an inline comment while preserving # inside quoted values. */
static void strip_comment(char *s) {
char quote = 0;
int escaped = 0;
for (; *s; s++) {
if (quote) {
if (escaped) {
escaped = 0;
} else if (*s == '\\') {
escaped = 1;
} else if (*s == quote) {
quote = 0;
}
} else if (*s == '\'' || *s == '"') {
quote = *s;
} else if (*s == '#') {
*s = 0;
return;
}
}
}

/* Strip matching quotes and decode only escaped quote/backslash pairs.
* Other backslashes remain literal so existing paths/passwords do not change. */
static void unquote(char *s) {
size_t len = strlen(s);
if (len < 2 || (s[0] != '\'' && s[0] != '"') || s[len - 1] != s[0])
return;

char quote = s[0];
char *src = s + 1;
char *end = s + len - 1;
char *dst = s;
while (src < end) {
if (*src == '\\' && src + 1 < end &&
(src[1] == quote || src[1] == '\\')) {
src++;
}
*dst++ = *src++;
}
*dst = 0;
}

static void set_str(char *dst, const char *src) {
snprintf(dst, CONFIG_STR_MAX, "%s", src);
}
Expand All @@ -24,28 +67,31 @@ int config_load(ssh_config_t *cfg, const char *path) {
set_str(cfg->user, "ubuntu");
set_str(cfg->key_path, "sdmc:/3ds/3dssh/id_rsa");
cfg->passphrase[0] = 0;
cfg->macos_keychain_password[0] = 0;

FILE *fp = fopen(path, "r");
if (!fp) return 0;

char line[CONFIG_STR_MAX * 2];
while (fgets(line, sizeof(line), fp)) {
char *hash = strchr(line, '#');
if (hash) *hash = 0;
strip_comment(line);
char *eq = strchr(line, '=');
if (!eq) continue;
*eq = 0;
char *key = line;
char *val = eq + 1;
trim(key);
trim(val);
unquote(val);
if (!*key) continue;

if (!strcmp(key, "host")) set_str(cfg->host, val);
else if (!strcmp(key, "port")) cfg->port = atoi(val);
else if (!strcmp(key, "user")) set_str(cfg->user, val);
else if (!strcmp(key, "key_path")) set_str(cfg->key_path, val);
else if (!strcmp(key, "passphrase")) set_str(cfg->passphrase, val);
else if (!strcmp(key, "macos_keychain_password"))
set_str(cfg->macos_keychain_password, val);
}
fclose(fp);
return 1;
Expand Down
2 changes: 2 additions & 0 deletions source/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
* user = ubuntu
* key_path = sdmc:/3ds/3dssh/id_rsa
* passphrase = # leave empty for unencrypted keys
* macos_keychain_password = # quote values containing # or edge spaces
*/

#define CONFIG_STR_MAX 256
Expand All @@ -21,6 +22,7 @@ typedef struct {
char user[CONFIG_STR_MAX];
char key_path[CONFIG_STR_MAX];
char passphrase[CONFIG_STR_MAX];
char macos_keychain_password[CONFIG_STR_MAX];
} ssh_config_t;

/* Fills cfg with defaults then overlays values from config file (if present).
Expand Down
8 changes: 8 additions & 0 deletions source/keychain_protocol.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#ifndef DSSH_KEYCHAIN_PROTOCOL_H
#define DSSH_KEYCHAIN_PROTOCOL_H

/* Match the emitted OSC control sequence, not the printable marker text in
* the interactive shell's echoed command line. */
#define DSSH_KEYCHAIN_RESULT_MARKER "\x1b]777;DSSH_KEYCHAIN_RESULT"

#endif /* DSSH_KEYCHAIN_PROTOCOL_H */
Loading