SFTP SSH 키 파일 로그인: 키 형식 · 키 암호 · 서버 거부를 따로 알림 · 고를 때 바로 꼴을 봄 · 키 종류마다 실제 서버 시험 (사용자 승인 10/21, 관리자 지시)

- 이미 있던 것 (그대로): 서버 설정 [개인 키 파일 고르기] · 키 암호 칸, 키 내용은 안전 저장소 (sftp.key.<id> · sftp.kp.<id>) 에 두고
  파일 경로는 저장하지 않음, 설정 파일 · .bak · 보관본에는 평문 없음 (redactSecrets). "개인 키 붙여넣기" 는 없어 합칠 것 없음. 셸은 넣지 않음
- 더한 것: SftpFailure 에 keyFormat · keyPassphrase · keyRejected
  · checkPrivateKey: OpenSSH · PEM RSA · PEM EC (dartssh2 4.1.0 이 읽는 꼴) 인지, 암호가 걸렸는지. PuTTY (.ppk) 는 PuTTYgen 으로 바꾸는 법,
    PKCS#8 · 공개 키 (.pub) · 다른 파일은 고를 꼴을 알림 - [개인 키 파일 고르기] 에서 바로 (암호가 걸린 키면 "아래에 키 암호를 넣으세요")
  · parsePrivateKey: 암호가 걸렸는데 비었으면 "키 암호를 넣으세요", 못 풀면 "암호가 맞지 않습니다" (PEM 의 다른 오류도), 그 밖은 꼴
  · 키만으로 들어가다 SSHAuthError 면 "서버가 이 키를 받지 않습니다 (authorized_keys · 아이디)", 비밀번호도 있으면 "모두 거부"
- 시험 (실제 paramiko 서버 · Windows ssh-keygen): ed25519 OpenSSH (암호 · 없음) · rsa OpenSSH 암호 · rsa PEM · ecdsa OpenSSH 암호 ·
  ecdsa PEM 6종으로 로그인, 짝이 없는 키 = keyRejected, 암호 없음 = keyPassphrase, PuTTY · PKCS#8 · 공개 키 · 다른 파일 = keyFormat.
  틀린 키 암호 시험은 kind 를 keyPassphrase 로. sftp · sftp_stream 14개 통과, l10n 빠진 것 0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rc2
RyuJongJin 2026-10-21 10:53:44 +09:00
parent 6c5aef75a2
commit 9f6c5c0bd5
6 changed files with 125 additions and 17 deletions

View File

@ -274,6 +274,7 @@
"PDF 를 만들지 못했습니다: {0}": "Could not make the PDF: {0}", "PDF 를 만들지 못했습니다: {0}": "Could not make the PDF: {0}",
"PDF 를 열 수 없습니다: {0}": "Cannot open the PDF: {0}", "PDF 를 열 수 없습니다: {0}": "Cannot open the PDF: {0}",
"PDF 에 넣을 그림 선택": "Choose images to add to the PDF", "PDF 에 넣을 그림 선택": "Choose images to add to the PDF",
"PuTTY 키 (.ppk) 는 쓸 수 없습니다. PuTTYgen 의 [Conversions] > [Export OpenSSH key] 로 바꾼 파일을 고르세요": "PuTTY keys (.ppk) can't be used. Choose a file converted with PuTTYgen [Conversions] > [Export OpenSSH key]",
"Real-ESRGAN x4plus (사진)": "Real-ESRGAN x4plus (photos)", "Real-ESRGAN x4plus (사진)": "Real-ESRGAN x4plus (photos)",
"Real-ESRGAN x4plus anime 6B (만화 · 그림, 가볍고 빠름)": "Real-ESRGAN x4plus anime 6B (comics · art, light and fast)", "Real-ESRGAN x4plus anime 6B (만화 · 그림, 가볍고 빠름)": "Real-ESRGAN x4plus anime 6B (comics · art, light and fast)",
"Rsync (지금 여기)": "Rsync (you are here)", "Rsync (지금 여기)": "Rsync (you are here)",
@ -699,11 +700,12 @@
"개인 키 암호": "Private key passphrase", "개인 키 암호": "Private key passphrase",
"개인 키 있음": "Private key set", "개인 키 있음": "Private key set",
"개인 키 파일 고르기": "Choose private key file", "개인 키 파일 고르기": "Choose private key file",
"개인 키 파일이 아닙니다 (OpenSSH · PEM 형식의 비밀 키를 고르세요, .pub 아님)": "This isn't a private key file (choose an OpenSSH or PEM private key, not .pub)",
"개인 키 파일이 아닙니다 (너무 큼)": "This isn't a private key file (too large)", "개인 키 파일이 아닙니다 (너무 큼)": "This isn't a private key file (too large)",
"개인 키를 넣었습니다 (원래 파일 경로는 저장하지 않습니다)": "Private key added (the original file path isn't saved)", "개인 키를 넣었습니다 (원래 파일 경로는 저장하지 않습니다)": "Private key added (the original file path isn't saved)",
"개인 키를 읽을 수 없습니다 (OpenSSH · PEM 형식)": "Can't read the private key (OpenSSH or PEM format)", "개인 키를 넣었습니다. 암호가 걸린 키라 아래에 키 암호를 넣으세요 (원래 파일 경로는 저장하지 않습니다)": "Private key added. It is protected by a passphrase, so enter the key passphrase below (the original file path is not saved)",
"개인 키를 읽을 수 없습니다. OpenSSH 형식이나 PEM (RSA · EC) 형식의 비밀 키 (ed25519 · rsa · ecdsa) 를 고르세요 (.pub 은 공개 키)": "Can't read the private key. Choose an OpenSSH or PEM (RSA · EC) private key (ed25519 · rsa · ecdsa) (.pub is the public key)",
"개인 키만 쓰면 비웁니다": "Leave empty if you use only a private key", "개인 키만 쓰면 비웁니다": "Leave empty if you use only a private key",
"개인 키에 암호가 걸려 있습니다. 키 암호를 넣으세요": "The private key is protected by a passphrase. Enter the key passphrase",
"개인 키의 암호가 맞지 않습니다": "The private key passphrase is incorrect", "개인 키의 암호가 맞지 않습니다": "The private key passphrase is incorrect",
"거꾸로": "Reverse", "거꾸로": "Reverse",
"거꾸로 된 범위: {0}": "Reversed range: {0}", "거꾸로 된 범위: {0}": "Reversed range: {0}",
@ -1581,6 +1583,7 @@
"서버가 암호화 (FTPS) 를 시작하기 전에 예상하지 못한 답을 보냈습니다. 연결을 끊었습니다": "The server sent an unexpected reply before starting encryption (FTPS). The connection was closed", "서버가 암호화 (FTPS) 를 시작하기 전에 예상하지 못한 답을 보냈습니다. 연결을 끊었습니다": "The server sent an unexpected reply before starting encryption (FTPS). The connection was closed",
"서버가 암호화 (FTPS) 를 하지 않습니다 ({0} {1}). 서버 설정에서 FTPS 를 켜거나, 위험을 알고 \"평문 FTP\" 를 고르세요": "The server doesn't support encryption (FTPS) ({0} {1}). Turn on FTPS on the server, or choose \"Plain FTP\" knowing the risk", "서버가 암호화 (FTPS) 를 하지 않습니다 ({0} {1}). 서버 설정에서 FTPS 를 켜거나, 위험을 알고 \"평문 FTP\" 를 고르세요": "The server doesn't support encryption (FTPS) ({0} {1}). Turn on FTPS on the server, or choose \"Plain FTP\" knowing the risk",
"서버가 이 기능을 허용하지 않음": "the server does not allow this", "서버가 이 기능을 허용하지 않음": "the server does not allow this",
"서버가 이 키를 받지 않습니다. 서버의 ~/.ssh/authorized_keys 에 짝이 되는 공개 키 (.pub) 가 있는지, 아이디가 맞는지 보세요": "The server doesn't accept this key. Check that the matching public key (.pub) is in the server's ~/.ssh/authorized_keys and that the user name is right",
"서버에 닿을 수 없음": "host unreachable", "서버에 닿을 수 없음": "host unreachable",
"서버에 닿지 않습니다": "Cannot reach the server", "서버에 닿지 않습니다": "Cannot reach the server",
"서버에서 이 계정의 권한을 확인하세요.": "Check this account's permissions on the server.", "서버에서 이 계정의 권한을 확인하세요.": "Check this account's permissions on the server.",
@ -1740,6 +1743,7 @@
"아이디 (비우면 게스트)": "User name (guest if empty)", "아이디 (비우면 게스트)": "User name (guest if empty)",
"아이디 (비우면 익명)": "User name (empty = anonymous)", "아이디 (비우면 익명)": "User name (empty = anonymous)",
"아이디 (선택)": "Username (optional)", "아이디 (선택)": "Username (optional)",
"아이디 · 비밀번호 · 키가 모두 서버에서 거부되었습니다": "The user name, password and key were all rejected by the server",
"아이디 · 비밀번호를 확인하세요": "check the user name and password", "아이디 · 비밀번호를 확인하세요": "check the user name and password",
"아이디 또는 비밀번호가 맞지 않습니다": "The user name or password is wrong", "아이디 또는 비밀번호가 맞지 않습니다": "The user name or password is wrong",
"아이디·비밀번호는 환경 설정에서 넣을 수 있습니다 (선택, 하루 받기 횟수가 늘어남).": "You can enter the username · password in Settings (optional, raises the daily download limit).", "아이디·비밀번호는 환경 설정에서 넣을 수 있습니다 (선택, 하루 받기 횟수가 늘어남).": "You can enter the username · password in Settings (optional, raises the daily download limit).",

View File

@ -274,6 +274,7 @@
"PDF 를 만들지 못했습니다: {0}": "PDF を作成できませんでした: {0}", "PDF 를 만들지 못했습니다: {0}": "PDF を作成できませんでした: {0}",
"PDF 를 열 수 없습니다: {0}": "PDF を開けません: {0}", "PDF 를 열 수 없습니다: {0}": "PDF を開けません: {0}",
"PDF 에 넣을 그림 선택": "PDF に追加する画像を選択", "PDF 에 넣을 그림 선택": "PDF に追加する画像を選択",
"PuTTY 키 (.ppk) 는 쓸 수 없습니다. PuTTYgen 의 [Conversions] > [Export OpenSSH key] 로 바꾼 파일을 고르세요": "PuTTY のキー (.ppk) は使えません。PuTTYgen の [Conversions] > [Export OpenSSH key] で変換したファイルを選んでください",
"Real-ESRGAN x4plus (사진)": "Real-ESRGAN x4plus (写真)", "Real-ESRGAN x4plus (사진)": "Real-ESRGAN x4plus (写真)",
"Real-ESRGAN x4plus anime 6B (만화 · 그림, 가볍고 빠름)": "Real-ESRGAN x4plus anime 6B (マンガ・イラスト、軽くて速い)", "Real-ESRGAN x4plus anime 6B (만화 · 그림, 가볍고 빠름)": "Real-ESRGAN x4plus anime 6B (マンガ・イラスト、軽くて速い)",
"Rsync (지금 여기)": "Rsync (現在地)", "Rsync (지금 여기)": "Rsync (現在地)",
@ -699,11 +700,12 @@
"개인 키 암호": "秘密鍵のパスフレーズ", "개인 키 암호": "秘密鍵のパスフレーズ",
"개인 키 있음": "秘密鍵あり", "개인 키 있음": "秘密鍵あり",
"개인 키 파일 고르기": "秘密鍵ファイルを選択", "개인 키 파일 고르기": "秘密鍵ファイルを選択",
"개인 키 파일이 아닙니다 (OpenSSH · PEM 형식의 비밀 키를 고르세요, .pub 아님)": "秘密鍵ファイルではありません (OpenSSH・PEM 形式の秘密鍵を選んでください。.pub ではありません)",
"개인 키 파일이 아닙니다 (너무 큼)": "秘密鍵ファイルではありません (大きすぎます)", "개인 키 파일이 아닙니다 (너무 큼)": "秘密鍵ファイルではありません (大きすぎます)",
"개인 키를 넣었습니다 (원래 파일 경로는 저장하지 않습니다)": "秘密鍵を登録しました (元のファイルのパスは保存しません)", "개인 키를 넣었습니다 (원래 파일 경로는 저장하지 않습니다)": "秘密鍵を登録しました (元のファイルのパスは保存しません)",
"개인 키를 읽을 수 없습니다 (OpenSSH · PEM 형식)": "秘密鍵を読み込めません (OpenSSH・PEM 形式)", "개인 키를 넣었습니다. 암호가 걸린 키라 아래에 키 암호를 넣으세요 (원래 파일 경로는 저장하지 않습니다)": "秘密鍵を入れました。パスフレーズ付きのキーなので下にキーのパスフレーズを入れてください (元のファイルの場所は保存しません)",
"개인 키를 읽을 수 없습니다. OpenSSH 형식이나 PEM (RSA · EC) 형식의 비밀 키 (ed25519 · rsa · ecdsa) 를 고르세요 (.pub 은 공개 키)": "秘密鍵を読めません。OpenSSH 形式か PEM (RSA・EC) 形式の秘密鍵 (ed25519・rsa・ecdsa) を選んでください (.pub は公開鍵)",
"개인 키만 쓰면 비웁니다": "秘密鍵だけを使うなら空にします", "개인 키만 쓰면 비웁니다": "秘密鍵だけを使うなら空にします",
"개인 키에 암호가 걸려 있습니다. 키 암호를 넣으세요": "秘密鍵にパスフレーズがかかっています。キーのパスフレーズを入れてください",
"개인 키의 암호가 맞지 않습니다": "秘密鍵のパスフレーズが正しくありません", "개인 키의 암호가 맞지 않습니다": "秘密鍵のパスフレーズが正しくありません",
"거꾸로": "逆順", "거꾸로": "逆順",
"거꾸로 된 범위: {0}": "逆順の範囲: {0}", "거꾸로 된 범위: {0}": "逆順の範囲: {0}",
@ -1581,6 +1583,7 @@
"서버가 암호화 (FTPS) 를 시작하기 전에 예상하지 못한 답을 보냈습니다. 연결을 끊었습니다": "サーバーが暗号化 (FTPS) の開始前に予期しない応答を送りました。接続を切りました", "서버가 암호화 (FTPS) 를 시작하기 전에 예상하지 못한 답을 보냈습니다. 연결을 끊었습니다": "サーバーが暗号化 (FTPS) の開始前に予期しない応答を送りました。接続を切りました",
"서버가 암호화 (FTPS) 를 하지 않습니다 ({0} {1}). 서버 설정에서 FTPS 를 켜거나, 위험을 알고 \"평문 FTP\" 를 고르세요": "サーバーが暗号化 (FTPS) に対応していません ({0} {1})。サーバー側で FTPS を有効にするか、リスクを理解した上で「平文 FTP」を選んでください", "서버가 암호화 (FTPS) 를 하지 않습니다 ({0} {1}). 서버 설정에서 FTPS 를 켜거나, 위험을 알고 \"평문 FTP\" 를 고르세요": "サーバーが暗号化 (FTPS) に対応していません ({0} {1})。サーバー側で FTPS を有効にするか、リスクを理解した上で「平文 FTP」を選んでください",
"서버가 이 기능을 허용하지 않음": "サーバーがこの機能を許可していません", "서버가 이 기능을 허용하지 않음": "サーバーがこの機能を許可していません",
"서버가 이 키를 받지 않습니다. 서버의 ~/.ssh/authorized_keys 에 짝이 되는 공개 키 (.pub) 가 있는지, 아이디가 맞는지 보세요": "サーバーがこのキーを受け付けません。サーバーの ~/.ssh/authorized_keys に対になる公開鍵 (.pub) があるか、ユーザー名が合っているか確認してください",
"서버에 닿을 수 없음": "ホストに到達できません", "서버에 닿을 수 없음": "ホストに到達できません",
"서버에 닿지 않습니다": "サーバーに接続できません", "서버에 닿지 않습니다": "サーバーに接続できません",
"서버에서 이 계정의 권한을 확인하세요.": "サーバーでこのアカウントの権限を確認してください。", "서버에서 이 계정의 권한을 확인하세요.": "サーバーでこのアカウントの権限を確認してください。",
@ -1740,6 +1743,7 @@
"아이디 (비우면 게스트)": "ユーザー名 (空欄ならゲスト)", "아이디 (비우면 게스트)": "ユーザー名 (空欄ならゲスト)",
"아이디 (비우면 익명)": "ユーザー名 (空欄なら匿名)", "아이디 (비우면 익명)": "ユーザー名 (空欄なら匿名)",
"아이디 (선택)": "ユーザー名(任意)", "아이디 (선택)": "ユーザー名(任意)",
"아이디 · 비밀번호 · 키가 모두 서버에서 거부되었습니다": "ユーザー名・パスワード・キーのすべてがサーバーに拒否されました",
"아이디 · 비밀번호를 확인하세요": "ID とパスワードを確認してください", "아이디 · 비밀번호를 확인하세요": "ID とパスワードを確認してください",
"아이디 또는 비밀번호가 맞지 않습니다": "ユーザー名またはパスワードが正しくありません", "아이디 또는 비밀번호가 맞지 않습니다": "ユーザー名またはパスワードが正しくありません",
"아이디·비밀번호는 환경 설정에서 넣을 수 있습니다 (선택, 하루 받기 횟수가 늘어남).": "ユーザー名・パスワードは環境設定で入力できます(任意、1日のダウンロード回数が増えます)。", "아이디·비밀번호는 환경 설정에서 넣을 수 있습니다 (선택, 하루 받기 횟수가 늘어남).": "ユーザー名・パスワードは環境設定で入力できます(任意、1日のダウンロード回数が増えます)。",

View File

@ -274,6 +274,7 @@
"PDF 를 만들지 못했습니다: {0}": "无法制作 PDF: {0}", "PDF 를 만들지 못했습니다: {0}": "无法制作 PDF: {0}",
"PDF 를 열 수 없습니다: {0}": "无法打开 PDF: {0}", "PDF 를 열 수 없습니다: {0}": "无法打开 PDF: {0}",
"PDF 에 넣을 그림 선택": "选择要加入 PDF 的图片", "PDF 에 넣을 그림 선택": "选择要加入 PDF 的图片",
"PuTTY 키 (.ppk) 는 쓸 수 없습니다. PuTTYgen 의 [Conversions] > [Export OpenSSH key] 로 바꾼 파일을 고르세요": "无法使用 PuTTY 密钥 (.ppk)。请选择用 PuTTYgen 的 [Conversions] > [Export OpenSSH key] 转换的文件",
"Real-ESRGAN x4plus (사진)": "Real-ESRGAN x4plus (照片)", "Real-ESRGAN x4plus (사진)": "Real-ESRGAN x4plus (照片)",
"Real-ESRGAN x4plus anime 6B (만화 · 그림, 가볍고 빠름)": "Real-ESRGAN x4plus anime 6B (漫画 · 插画,轻量快速)", "Real-ESRGAN x4plus anime 6B (만화 · 그림, 가볍고 빠름)": "Real-ESRGAN x4plus anime 6B (漫画 · 插画,轻量快速)",
"Rsync (지금 여기)": "Rsync (当前位置)", "Rsync (지금 여기)": "Rsync (当前位置)",
@ -699,11 +700,12 @@
"개인 키 암호": "私钥密码", "개인 키 암호": "私钥密码",
"개인 키 있음": "已设置私钥", "개인 키 있음": "已设置私钥",
"개인 키 파일 고르기": "选择私钥文件", "개인 키 파일 고르기": "选择私钥文件",
"개인 키 파일이 아닙니다 (OpenSSH · PEM 형식의 비밀 키를 고르세요, .pub 아님)": "这不是私钥文件 (请选择 OpenSSH · PEM 格式的私钥,而不是 .pub)",
"개인 키 파일이 아닙니다 (너무 큼)": "这不是私钥文件 (文件过大)", "개인 키 파일이 아닙니다 (너무 큼)": "这不是私钥文件 (文件过大)",
"개인 키를 넣었습니다 (원래 파일 경로는 저장하지 않습니다)": "已添加私钥 (不保存原文件路径)", "개인 키를 넣었습니다 (원래 파일 경로는 저장하지 않습니다)": "已添加私钥 (不保存原文件路径)",
"개인 키를 읽을 수 없습니다 (OpenSSH · PEM 형식)": "无法读取私钥 (OpenSSH · PEM 格式)", "개인 키를 넣었습니다. 암호가 걸린 키라 아래에 키 암호를 넣으세요 (원래 파일 경로는 저장하지 않습니다)": "已添加私钥。此密钥设有密码,请在下方输入私钥密码 (不保存原文件路径)",
"개인 키를 읽을 수 없습니다. OpenSSH 형식이나 PEM (RSA · EC) 형식의 비밀 키 (ed25519 · rsa · ecdsa) 를 고르세요 (.pub 은 공개 키)": "无法读取私钥。请选择 OpenSSH 或 PEM (RSA · EC) 格式的私钥 (ed25519 · rsa · ecdsa) (.pub 是公钥)",
"개인 키만 쓰면 비웁니다": "仅使用私钥时留空", "개인 키만 쓰면 비웁니다": "仅使用私钥时留空",
"개인 키에 암호가 걸려 있습니다. 키 암호를 넣으세요": "私钥设有密码。请输入私钥密码",
"개인 키의 암호가 맞지 않습니다": "私钥密码不正确", "개인 키의 암호가 맞지 않습니다": "私钥密码不正确",
"거꾸로": "倒序", "거꾸로": "倒序",
"거꾸로 된 범위: {0}": "范围顺序颠倒: {0}", "거꾸로 된 범위: {0}": "范围顺序颠倒: {0}",
@ -1581,6 +1583,7 @@
"서버가 암호화 (FTPS) 를 시작하기 전에 예상하지 못한 답을 보냈습니다. 연결을 끊었습니다": "服务器在开始加密 (FTPS) 前发送了意外的回复。已断开连接", "서버가 암호화 (FTPS) 를 시작하기 전에 예상하지 못한 답을 보냈습니다. 연결을 끊었습니다": "服务器在开始加密 (FTPS) 前发送了意外的回复。已断开连接",
"서버가 암호화 (FTPS) 를 하지 않습니다 ({0} {1}). 서버 설정에서 FTPS 를 켜거나, 위험을 알고 \"평문 FTP\" 를 고르세요": "服务器不支持加密 (FTPS) ({0} {1})。请在服务器上启用 FTPS,或在了解风险后选择\"明文 FTP\"", "서버가 암호화 (FTPS) 를 하지 않습니다 ({0} {1}). 서버 설정에서 FTPS 를 켜거나, 위험을 알고 \"평문 FTP\" 를 고르세요": "服务器不支持加密 (FTPS) ({0} {1})。请在服务器上启用 FTPS,或在了解风险后选择\"明文 FTP\"",
"서버가 이 기능을 허용하지 않음": "服务器不允许此操作", "서버가 이 기능을 허용하지 않음": "服务器不允许此操作",
"서버가 이 키를 받지 않습니다. 서버의 ~/.ssh/authorized_keys 에 짝이 되는 공개 키 (.pub) 가 있는지, 아이디가 맞는지 보세요": "服务器不接受此密钥。请检查服务器的 ~/.ssh/authorized_keys 中是否有对应的公钥 (.pub),以及用户名是否正确",
"서버에 닿을 수 없음": "主机不可达", "서버에 닿을 수 없음": "主机不可达",
"서버에 닿지 않습니다": "无法连接服务器", "서버에 닿지 않습니다": "无法连接服务器",
"서버에서 이 계정의 권한을 확인하세요.": "请在服务器上检查此账户的权限。", "서버에서 이 계정의 권한을 확인하세요.": "请在服务器上检查此账户的权限。",
@ -1740,6 +1743,7 @@
"아이디 (비우면 게스트)": "用户名 (留空则为访客)", "아이디 (비우면 게스트)": "用户名 (留空则为访客)",
"아이디 (비우면 익명)": "用户名 (留空则匿名)", "아이디 (비우면 익명)": "用户名 (留空则匿名)",
"아이디 (선택)": "用户名(可选)", "아이디 (선택)": "用户名(可选)",
"아이디 · 비밀번호 · 키가 모두 서버에서 거부되었습니다": "用户名、密码和密钥都被服务器拒绝",
"아이디 · 비밀번호를 확인하세요": "请检查用户名和密码", "아이디 · 비밀번호를 확인하세요": "请检查用户名和密码",
"아이디 또는 비밀번호가 맞지 않습니다": "用户名或密码错误", "아이디 또는 비밀번호가 맞지 않습니다": "用户名或密码错误",
"아이디·비밀번호는 환경 설정에서 넣을 수 있습니다 (선택, 하루 받기 횟수가 늘어남).": "可在设置中填写用户名 · 密码(可选,可增加每日下载次数)。", "아이디·비밀번호는 환경 설정에서 넣을 수 있습니다 (선택, 하루 받기 횟수가 늘어남).": "可在设置中填写用户名 · 密码(可选,可增加每日下载次数)。",

View File

@ -113,7 +113,32 @@ class SftpServer {
} }
/// SFTP 실패의 종류 (안내 글 · 할 일 단추를 고르는 데) /// SFTP 실패의 종류 (안내 글 · 할 일 단추를 고르는 데)
enum SftpFailure { unreachable, auth, noSecret, hostKeyUnknown, hostKeyChanged, permission, notFound, other } /// [keyFormat] · [keyPassphrase] · [keyRejected]: SSH 키 로그인의 실패를 따로 (관리자 지시 10/21 - 연결 확인에서 키 형식 · 암호 틀림 · 서버 거부를 나눠 알림)
enum SftpFailure { unreachable, auth, noSecret, hostKeyUnknown, hostKeyChanged, permission, notFound, keyFormat, keyPassphrase, keyRejected, other }
/// 개인 키 글이 쓸 수 있는 꼴인지 본다 (OpenSSH · PEM RSA · PEM EC - dartssh2 가 읽는 것). 암호가 걸려 있으면 true.
/// 쓸 수 없으면 [SftpException] ([SftpFailure.keyFormat]) - PuTTY (.ppk) · PKCS#8 (BEGIN PRIVATE KEY) · 공개 키는 바꾸는 법을 알린다
bool checkPrivateKey(String text) {
if (text.contains('PuTTY-User-Key-File')) throw SftpException(tr('PuTTY 키 (.ppk) 는 쓸 수 없습니다. PuTTYgen 의 [Conversions] > [Export OpenSSH key] 로 바꾼 파일을 고르세요'), SftpFailure.keyFormat);
try {
return SSHKeyPair.isEncryptedPem(text);
} catch (_) {
throw SftpException(tr('개인 키를 읽을 수 없습니다. OpenSSH 형식이나 PEM (RSA · EC) 형식의 비밀 키 (ed25519 · rsa · ecdsa) 를 고르세요 (.pub 은 공개 키)'), SftpFailure.keyFormat);
}
}
/// 개인 키 글 + 키 암호 → 로그인에 쓸 키. 키 형식 · 암호 없음 · 암호 틀림을 따로 알린다
List<SSHKeyPair> parsePrivateKey(String text, String passphrase) {
final encrypted = checkPrivateKey(text);
if (encrypted && passphrase.isEmpty) throw SftpException(tr('개인 키에 암호가 걸려 있습니다. 키 암호를 넣으세요'), SftpFailure.keyPassphrase);
try {
return SSHKeyPair.fromPem(text, encrypted ? passphrase : null);
} catch (_) {
// 암호가 걸린 키를 풀지 못함 = 암호 틀림 (PEM 은 틀린 암호로 풀면 다른 오류가 나기도 한다)
if (encrypted) throw SftpException(tr('개인 키의 암호가 맞지 않습니다'), SftpFailure.keyPassphrase);
throw SftpException(tr('개인 키를 읽을 수 없습니다. OpenSSH 형식이나 PEM (RSA · EC) 형식의 비밀 키 (ed25519 · rsa · ecdsa) 를 고르세요 (.pub 은 공개 키)'), SftpFailure.keyFormat);
}
}
/// SFTP 오류 - WebDAV 오류와 같은 곳에서 보이도록 [DavException] 을 잇는다 /// SFTP 오류 - WebDAV 오류와 같은 곳에서 보이도록 [DavException] 을 잇는다
class SftpException extends DavException { class SftpException extends DavException {
@ -187,13 +212,10 @@ class SftpRemote implements RemoteClient {
List<SSHKeyPair>? keys; List<SSHKeyPair>? keys;
if (s.privateKey.isNotEmpty) { if (s.privateKey.isNotEmpty) {
try { try {
keys = SSHKeyPair.fromPem(s.privateKey, s.keyPassphrase.isEmpty ? null : s.keyPassphrase); keys = parsePrivateKey(s.privateKey, s.keyPassphrase);
} on SSHKeyDecryptError {
sock.destroy();
throw SftpException(tr('개인 키의 암호가 맞지 않습니다'), SftpFailure.auth);
} catch (_) { } catch (_) {
sock.destroy(); sock.destroy();
throw SftpException(tr('개인 키를 읽을 수 없습니다 (OpenSSH · PEM 형식)'), SftpFailure.auth); rethrow;
} }
} }
final ssh = SSHClient( final ssh = SSHClient(
@ -250,7 +272,12 @@ class SftpRemote implements RemoteClient {
: tr('서버와 암호 방식을 맞추지 못했습니다. 오래된 서버면 서버 설정의 "오래된 ssh-rsa 허용" 을 켜 보세요'), : tr('서버와 암호 방식을 맞추지 못했습니다. 오래된 서버면 서버 설정의 "오래된 ssh-rsa 허용" 을 켜 보세요'),
SftpFailure.other); SftpFailure.other);
} }
if (e is SSHAuthError) return SftpException(tr('아이디나 비밀번호 (또는 키) 가 맞지 않습니다'), SftpFailure.auth); if (e is SSHAuthError) {
// 관리자 지시 (10/21): 키만으로 들어가다 거부되면 서버가 그 키를 모르는 것 - 비밀번호와 따로 알린다
if (s.privateKey.isNotEmpty && s.password.isEmpty) return SftpException(tr('서버가 이 키를 받지 않습니다. 서버의 ~/.ssh/authorized_keys 에 짝이 되는 공개 키 (.pub) 가 있는지, 아이디가 맞는지 보세요'), SftpFailure.keyRejected);
if (s.privateKey.isNotEmpty) return SftpException(tr('아이디 · 비밀번호 · 키가 모두 서버에서 거부되었습니다'), SftpFailure.auth);
return SftpException(tr('아이디나 비밀번호 (또는 키) 가 맞지 않습니다'), SftpFailure.auth);
}
return SftpException('$e', SftpFailure.other); return SftpException('$e', SftpFailure.other);
} }

View File

@ -227,15 +227,16 @@ Future<SftpServer?> editSftpServer(BuildContext context, AppController c, {SftpS
// 개인 키는 작다 - 큰 파일은 키가 아니다 (실수로 다른 파일을 고름) // 개인 키는 작다 - 큰 파일은 키가 아니다 (실수로 다른 파일을 고름)
if (await f.length() > 64 * 1024) throw FormatException(tr('개인 키 파일이 아닙니다 (너무 큼)')); if (await f.length() > 64 * 1024) throw FormatException(tr('개인 키 파일이 아닙니다 (너무 큼)'));
final text = await f.readAsString(); final text = await f.readAsString();
if (!text.contains('PRIVATE KEY')) throw FormatException(tr('개인 키 파일이 아닙니다 (OpenSSH · PEM 형식의 비밀 키를 고르세요, .pub 아님)')); // 관리자 지시 (10/21): 고를 때 바로 꼴을 본다 (OpenSSH · PEM RSA · EC - PuTTY · PKCS#8 · 공개 키는 바꾸는 법을 알림)
final encrypted = checkPrivateKey(text);
set(() { set(() {
key = text; key = text;
result = tr('개인 키를 넣었습니다 (원래 파일 경로는 저장하지 않습니다)'); result = encrypted ? tr('개인 키를 넣었습니다. 암호가 걸린 키라 아래에 키 암호를 넣으세요 (원래 파일 경로는 저장하지 않습니다)') : tr('개인 키를 넣었습니다 (원래 파일 경로는 저장하지 않습니다)');
passed = true; passed = true;
}); });
} catch (e) { } catch (e) {
set(() { set(() {
result = e is FormatException ? e.message : '$e'; result = e is FormatException ? e.message : e is SftpException ? e.message : '$e';
passed = false; passed = false;
}); });
} }

View File

@ -299,10 +299,78 @@ void main() {
addTearDown(c.close); addTearDown(c.close);
expect(await c.list('/'), isEmpty); expect(await c.list('/'), isEmpty);
final bad = await err(() => SftpRemote(srv(password: '', key: pem, kp: 'nope', fp: fingerprint)).list('/')); final bad = await err(() => SftpRemote(srv(password: '', key: pem, kp: 'nope', fp: fingerprint)).list('/'));
expect((bad! as SftpException).kind, SftpFailure.auth); expect((bad! as SftpException).kind, SftpFailure.keyPassphrase);
expect('$bad', contains('개인 키의 암호')); expect('$bad', contains('개인 키의 암호'));
}, skip: hasServer && File(keygen).existsSync() ? false : 'SFTP 시험 서버 · ssh-keygen 없음'); }, skip: hasServer && File(keygen).existsSync() ? false : 'SFTP 시험 서버 · ssh-keygen 없음');
test('관리자 지시 (10/21): SSH 키 파일 로그인 - ed25519 · rsa · ecdsa × OpenSSH · PEM (암호 있음 · 없음) 이 실제 서버에 붙고, 서버 거부 · 암호 없음은 따로', () async {
final dir = Directory(p.join(tmp.path, 'keys'))..createSync();
Future<String?> gen(String name, List<String> args) async {
final f = p.join(dir.path, name);
final r = await Process.run(keygen, ['-q', ...args, '-f', f, '-C', 'jj-test']);
return r.exitCode == 0 ? f : null;
}
final cases = [
('ed25519 OpenSSH 암호', ['-t', 'ed25519', '-N', 'kp'], 'kp'),
('ed25519 OpenSSH 암호 없음', ['-t', 'ed25519', '-N', ''], ''),
('rsa OpenSSH 암호', ['-t', 'rsa', '-b', '2048', '-N', 'kp'], 'kp'),
('rsa PEM 암호 없음', ['-t', 'rsa', '-b', '2048', '-m', 'PEM', '-N', ''], ''),
('ecdsa OpenSSH 암호', ['-t', 'ecdsa', '-b', '256', '-N', 'kp'], 'kp'),
('ecdsa PEM 암호 없음', ['-t', 'ecdsa', '-b', '256', '-m', 'PEM', '-N', ''], ''),
];
final failed = <String>[];
for (final (i, (name, args, kp)) in cases.indexed) {
final key = await gen('k$i', args);
if (key == null) return markTestSkipped('ssh-keygen 이 $name 을 만들지 못함');
await server?.stop();
await start(password: null, pubkey: '$key.pub');
final pem = File(key).readAsStringSync();
expect(checkPrivateKey(pem), kp.isNotEmpty, reason: '$name: 암호가 걸렸는지');
final c = SftpRemote(srv(password: '', key: pem, kp: kp, fp: fingerprint));
try {
await c.list('/');
} catch (e) {
failed.add('$name: $e');
} finally {
c.close();
}
}
expect(failed, isEmpty, reason: failed.join('\n'));
// 서버가 모르는 키 (짝 공개 키가 서버에 없음) → 키 거부 (비밀번호 틀림과 따로)
final other = await gen('other', ['-t', 'ed25519', '-N', '']);
final rejected = await err(() => SftpRemote(srv(password: '', key: File(other!).readAsStringSync(), fp: fingerprint)).list('/'));
expect((rejected! as SftpException).kind, SftpFailure.keyRejected, reason: '$rejected');
expect('$rejected', contains('authorized_keys'));
// 암호가 걸린 키인데 키 암호를 넣지 않음 → 그렇다고 (서버에 묻기 전에)
final locked = File(p.join(dir.path, 'k0')).readAsStringSync();
final noPass = await err(() => SftpRemote(srv(password: '', key: locked, fp: fingerprint)).list('/'));
expect((noPass! as SftpException).kind, SftpFailure.keyPassphrase);
expect('$noPass', contains('키 암호를 넣으세요'));
}, skip: hasServer && File(keygen).existsSync() ? false : 'SFTP 시험 서버 · ssh-keygen 없음', timeout: const Timeout(Duration(minutes: 3)));
test('관리자 지시 (10/21): 쓸 수 없는 키 꼴은 고를 때 바로 - PuTTY (.ppk) · PKCS#8 · 공개 키 · 다른 파일', () async {
final dir = Directory(p.join(tmp.path, 'keys'))..createSync();
final f = p.join(dir.path, 'p8');
final r = await Process.run(keygen, ['-q', '-t', 'rsa', '-b', '2048', '-m', 'PKCS8', '-N', '', '-f', f]);
final samples = {
'PuTTY': 'PuTTY-User-Key-File-3: ssh-ed25519\nEncryption: none\nComment: x\nPublic-Lines: 1\nAAAA\n',
if (r.exitCode == 0) 'PKCS#8': File(f).readAsStringSync(),
if (r.exitCode == 0) '공개 키': File('$f.pub').readAsStringSync(),
'다른 파일': 'hello',
};
for (final MapEntry(key: name, value: text) in samples.entries) {
Object? e;
try {
checkPrivateKey(text);
} catch (x) {
e = x;
}
expect(e, isA<SftpException>().having((x) => x.kind, 'kind', SftpFailure.keyFormat), reason: name);
if (name == 'PuTTY') expect('$e', contains('Export OpenSSH key'));
}
}, skip: File(keygen).existsSync() ? false : 'ssh-keygen 없음');
test('오래된 서버 (ssh-rsa 만): 처음엔 연결 안 됨 + 켜는 법 안내 → 서버마다 켜면 연결 (88-3)', () async { test('오래된 서버 (ssh-rsa 만): 처음엔 연결 안 됨 + 켜는 법 안내 → 서버마다 켜면 연결 (88-3)', () async {
await start(keytype: 'rsa', legacyOnly: true); await start(keytype: 'rsa', legacyOnly: true);
final e = await err(() => SftpRemote(srv(fp: fingerprint)).list('/')); final e = await err(() => SftpRemote(srv(fp: fingerprint)).list('/'));