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
parent
6c5aef75a2
commit
9f6c5c0bd5
|
|
@ -274,6 +274,7 @@
|
|||
"PDF 를 만들지 못했습니다: {0}": "Could not make the PDF: {0}",
|
||||
"PDF 를 열 수 없습니다: {0}": "Cannot open the PDF: {0}",
|
||||
"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 anime 6B (만화 · 그림, 가볍고 빠름)": "Real-ESRGAN x4plus anime 6B (comics · art, light and fast)",
|
||||
"Rsync (지금 여기)": "Rsync (you are here)",
|
||||
|
|
@ -699,11 +700,12 @@
|
|||
"개인 키 암호": "Private key passphrase",
|
||||
"개인 키 있음": "Private key set",
|
||||
"개인 키 파일 고르기": "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)",
|
||||
"개인 키를 넣었습니다 (원래 파일 경로는 저장하지 않습니다)": "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",
|
||||
"개인 키에 암호가 걸려 있습니다. 키 암호를 넣으세요": "The private key is protected by a passphrase. Enter the key passphrase",
|
||||
"개인 키의 암호가 맞지 않습니다": "The private key passphrase is incorrect",
|
||||
"거꾸로": "Reverse",
|
||||
"거꾸로 된 범위: {0}": "Reversed range: {0}",
|
||||
|
|
@ -1581,6 +1583,7 @@
|
|||
"서버가 암호화 (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",
|
||||
"서버가 이 기능을 허용하지 않음": "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",
|
||||
"서버에 닿지 않습니다": "Cannot reach the server",
|
||||
"서버에서 이 계정의 권한을 확인하세요.": "Check this account's permissions on the server.",
|
||||
|
|
@ -1740,6 +1743,7 @@
|
|||
"아이디 (비우면 게스트)": "User name (guest if empty)",
|
||||
"아이디 (비우면 익명)": "User name (empty = anonymous)",
|
||||
"아이디 (선택)": "Username (optional)",
|
||||
"아이디 · 비밀번호 · 키가 모두 서버에서 거부되었습니다": "The user name, password and key were all rejected by the server",
|
||||
"아이디 · 비밀번호를 확인하세요": "check the user name and password",
|
||||
"아이디 또는 비밀번호가 맞지 않습니다": "The user name or password is wrong",
|
||||
"아이디·비밀번호는 환경 설정에서 넣을 수 있습니다 (선택, 하루 받기 횟수가 늘어남).": "You can enter the username · password in Settings (optional, raises the daily download limit).",
|
||||
|
|
|
|||
|
|
@ -274,6 +274,7 @@
|
|||
"PDF 를 만들지 못했습니다: {0}": "PDF を作成できませんでした: {0}",
|
||||
"PDF 를 열 수 없습니다: {0}": "PDF を開けません: {0}",
|
||||
"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 anime 6B (만화 · 그림, 가볍고 빠름)": "Real-ESRGAN x4plus anime 6B (マンガ・イラスト、軽くて速い)",
|
||||
"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}",
|
||||
|
|
@ -1581,6 +1583,7 @@
|
|||
"서버가 암호화 (FTPS) 를 시작하기 전에 예상하지 못한 답을 보냈습니다. 연결을 끊었습니다": "サーバーが暗号化 (FTPS) の開始前に予期しない応答を送りました。接続を切りました",
|
||||
"서버가 암호화 (FTPS) 를 하지 않습니다 ({0} {1}). 서버 설정에서 FTPS 를 켜거나, 위험을 알고 \"평문 FTP\" 를 고르세요": "サーバーが暗号化 (FTPS) に対応していません ({0} {1})。サーバー側で FTPS を有効にするか、リスクを理解した上で「平文 FTP」を選んでください",
|
||||
"서버가 이 기능을 허용하지 않음": "サーバーがこの機能を許可していません",
|
||||
"서버가 이 키를 받지 않습니다. 서버의 ~/.ssh/authorized_keys 에 짝이 되는 공개 키 (.pub) 가 있는지, 아이디가 맞는지 보세요": "サーバーがこのキーを受け付けません。サーバーの ~/.ssh/authorized_keys に対になる公開鍵 (.pub) があるか、ユーザー名が合っているか確認してください",
|
||||
"서버에 닿을 수 없음": "ホストに到達できません",
|
||||
"서버에 닿지 않습니다": "サーバーに接続できません",
|
||||
"서버에서 이 계정의 권한을 확인하세요.": "サーバーでこのアカウントの権限を確認してください。",
|
||||
|
|
@ -1740,6 +1743,7 @@
|
|||
"아이디 (비우면 게스트)": "ユーザー名 (空欄ならゲスト)",
|
||||
"아이디 (비우면 익명)": "ユーザー名 (空欄なら匿名)",
|
||||
"아이디 (선택)": "ユーザー名(任意)",
|
||||
"아이디 · 비밀번호 · 키가 모두 서버에서 거부되었습니다": "ユーザー名・パスワード・キーのすべてがサーバーに拒否されました",
|
||||
"아이디 · 비밀번호를 확인하세요": "ID とパスワードを確認してください",
|
||||
"아이디 또는 비밀번호가 맞지 않습니다": "ユーザー名またはパスワードが正しくありません",
|
||||
"아이디·비밀번호는 환경 설정에서 넣을 수 있습니다 (선택, 하루 받기 횟수가 늘어남).": "ユーザー名・パスワードは環境設定で入力できます(任意、1日のダウンロード回数が増えます)。",
|
||||
|
|
|
|||
|
|
@ -274,6 +274,7 @@
|
|||
"PDF 를 만들지 못했습니다: {0}": "无法制作 PDF: {0}",
|
||||
"PDF 를 열 수 없습니다: {0}": "无法打开 PDF: {0}",
|
||||
"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 anime 6B (만화 · 그림, 가볍고 빠름)": "Real-ESRGAN x4plus anime 6B (漫画 · 插画,轻量快速)",
|
||||
"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}",
|
||||
|
|
@ -1581,6 +1583,7 @@
|
|||
"서버가 암호화 (FTPS) 를 시작하기 전에 예상하지 못한 답을 보냈습니다. 연결을 끊었습니다": "服务器在开始加密 (FTPS) 前发送了意外的回复。已断开连接",
|
||||
"서버가 암호화 (FTPS) 를 하지 않습니다 ({0} {1}). 서버 설정에서 FTPS 를 켜거나, 위험을 알고 \"평문 FTP\" 를 고르세요": "服务器不支持加密 (FTPS) ({0} {1})。请在服务器上启用 FTPS,或在了解风险后选择\"明文 FTP\"",
|
||||
"서버가 이 기능을 허용하지 않음": "服务器不允许此操作",
|
||||
"서버가 이 키를 받지 않습니다. 서버의 ~/.ssh/authorized_keys 에 짝이 되는 공개 키 (.pub) 가 있는지, 아이디가 맞는지 보세요": "服务器不接受此密钥。请检查服务器的 ~/.ssh/authorized_keys 中是否有对应的公钥 (.pub),以及用户名是否正确",
|
||||
"서버에 닿을 수 없음": "主机不可达",
|
||||
"서버에 닿지 않습니다": "无法连接服务器",
|
||||
"서버에서 이 계정의 권한을 확인하세요.": "请在服务器上检查此账户的权限。",
|
||||
|
|
@ -1740,6 +1743,7 @@
|
|||
"아이디 (비우면 게스트)": "用户名 (留空则为访客)",
|
||||
"아이디 (비우면 익명)": "用户名 (留空则匿名)",
|
||||
"아이디 (선택)": "用户名(可选)",
|
||||
"아이디 · 비밀번호 · 키가 모두 서버에서 거부되었습니다": "用户名、密码和密钥都被服务器拒绝",
|
||||
"아이디 · 비밀번호를 확인하세요": "请检查用户名和密码",
|
||||
"아이디 또는 비밀번호가 맞지 않습니다": "用户名或密码错误",
|
||||
"아이디·비밀번호는 환경 설정에서 넣을 수 있습니다 (선택, 하루 받기 횟수가 늘어남).": "可在设置中填写用户名 · 密码(可选,可增加每日下载次数)。",
|
||||
|
|
|
|||
|
|
@ -113,7 +113,32 @@ class SftpServer {
|
|||
}
|
||||
|
||||
/// 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] 을 잇는다
|
||||
class SftpException extends DavException {
|
||||
|
|
@ -187,13 +212,10 @@ class SftpRemote implements RemoteClient {
|
|||
List<SSHKeyPair>? keys;
|
||||
if (s.privateKey.isNotEmpty) {
|
||||
try {
|
||||
keys = SSHKeyPair.fromPem(s.privateKey, s.keyPassphrase.isEmpty ? null : s.keyPassphrase);
|
||||
} on SSHKeyDecryptError {
|
||||
sock.destroy();
|
||||
throw SftpException(tr('개인 키의 암호가 맞지 않습니다'), SftpFailure.auth);
|
||||
keys = parsePrivateKey(s.privateKey, s.keyPassphrase);
|
||||
} catch (_) {
|
||||
sock.destroy();
|
||||
throw SftpException(tr('개인 키를 읽을 수 없습니다 (OpenSSH · PEM 형식)'), SftpFailure.auth);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
final ssh = SSHClient(
|
||||
|
|
@ -250,7 +272,12 @@ class SftpRemote implements RemoteClient {
|
|||
: tr('서버와 암호 방식을 맞추지 못했습니다. 오래된 서버면 서버 설정의 "오래된 ssh-rsa 허용" 을 켜 보세요'),
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -227,15 +227,16 @@ Future<SftpServer?> editSftpServer(BuildContext context, AppController c, {SftpS
|
|||
// 개인 키는 작다 - 큰 파일은 키가 아니다 (실수로 다른 파일을 고름)
|
||||
if (await f.length() > 64 * 1024) throw FormatException(tr('개인 키 파일이 아닙니다 (너무 큼)'));
|
||||
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(() {
|
||||
key = text;
|
||||
result = tr('개인 키를 넣었습니다 (원래 파일 경로는 저장하지 않습니다)');
|
||||
result = encrypted ? tr('개인 키를 넣었습니다. 암호가 걸린 키라 아래에 키 암호를 넣으세요 (원래 파일 경로는 저장하지 않습니다)') : tr('개인 키를 넣었습니다 (원래 파일 경로는 저장하지 않습니다)');
|
||||
passed = true;
|
||||
});
|
||||
} catch (e) {
|
||||
set(() {
|
||||
result = e is FormatException ? e.message : '$e';
|
||||
result = e is FormatException ? e.message : e is SftpException ? e.message : '$e';
|
||||
passed = false;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -299,10 +299,78 @@ void main() {
|
|||
addTearDown(c.close);
|
||||
expect(await c.list('/'), isEmpty);
|
||||
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('개인 키의 암호'));
|
||||
}, 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 {
|
||||
await start(keytype: 'rsa', legacyOnly: true);
|
||||
final e = await err(() => SftpRemote(srv(fp: fingerprint)).list('/'));
|
||||
|
|
|
|||
Loading…
Reference in New Issue