FinalShell密码找回:从本地存储到Java解码的完整实践 1. FinalShell密码找回的背景与原理FinalShell作为一款常用的SSH客户端工具确实给运维工作带来了不少便利。但有时候我们会遇到这样的尴尬情况服务器密码明明保存在FinalShell里时间一长却忘记了原始密码。这时候如果直接重置服务器密码可能会影响线上服务而FinalShell的密码又是加密存储的直接查看配置文件只能看到一串乱码。其实FinalShell的密码加密机制并不复杂它采用的是DES加密算法配合随机密钥生成的技术方案。密码会被加密后存储在用户目录下的配置文件中路径通常是C:\Users\你的用户名\AppData\Local\finalshell\conn。每个服务器连接都会对应一个JSON文件里面就包含了加密后的密码字段。我遇到过好几次同事离职交接时忘记提供服务器密码的情况最后都是通过解析这些配置文件找回的密码。这个方法虽然有点黑客的感觉但确实是官方没有提供密码找回功能时的无奈之举。需要注意的是这种方法只能找回已经保存在FinalShell里的密码如果密码从未保存过那就无能为力了。2. 定位加密密码的存储位置要找回密码第一步就是找到FinalShell存储密码的配置文件。在Windows系统上这些文件默认存放在用户目录下的AppData文件夹中。具体路径是C:\Users\你的用户名\AppData\Local\finalshell\conn如果你找不到这个路径可能是因为AppData文件夹默认是隐藏的。这时候需要在文件资源管理器中开启显示隐藏的文件、文件夹和驱动器选项。具体操作是打开任意文件夹 → 点击查看选项卡 → 勾选隐藏的项目。进入conn文件夹后你会看到一堆以.json结尾的文件。这些文件对应着你保存在FinalShell中的各个服务器连接。每个文件的命名规则不太一样有的是以服务器IP命名有的是以连接名称命名需要你自己辨别哪个是你要找的服务器。用文本编辑器打开对应的json文件后搜索password字段你会看到类似这样的内容password: Pn1vK14tShb4G7ByTjidNtT/EoQ8ic6f这串看起来像乱码的字符串就是加密后的密码。我们需要做的就是把它复制出来然后通过Java程序进行解密。3. Java解密程序的原理分析FinalShell使用的加密算法是DES这是一种对称加密算法。但它的实现方式有点特殊不是简单的固定密钥加密而是结合了随机数生成技术使得每次加密的结果都不一样。我仔细研究过这个解密程序的代码发现它的核心逻辑可以分为几个部分Base64解码首先将加密字符串进行Base64解码转换成字节数组提取头信息前8个字节是加密时使用的随机头信息生成解密密钥通过一个复杂的随机数生成算法从头信息中派生出实际的解密密钥DES解密使用生成的密钥对剩余的数据进行DES解密MD5校验解密过程中还涉及到MD5哈希计算用于密钥的最终处理这个过程中最精妙的部分是密钥生成算法。它使用了一个固定的大数3680984568597093857L除以一个基于头信息生成的随机数然后再通过一系列随机数运算最终生成8个long型数值拼接起来再做MD5哈希才得到真正的DES密钥。这种设计使得即使你知道加密算法如果没有正确的密钥生成逻辑也无法破解密码。这也是为什么网上很难找到现成的解密工具必须使用这个特定的Java程序。4. 完整Java解密代码实现下面是我整理优化后的完整Java解密代码相比原始版本做了一些改进增加了错误处理和日志输出使用起来更加友好import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; import java.io.IOException; import java.math.BigInteger; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Base64; import java.util.Random; import javax.crypto.Cipher; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.DESKeySpec; public class FinalShellDecodePass { public static void main(String[] args) { if (args.length 0) { System.out.println(请提供加密密码作为参数); return; } try { System.out.println(解密结果: decodePass(args[0])); } catch (Exception e) { System.err.println(解密失败: e.getMessage()); e.printStackTrace(); } } public static String decodePass(String data) throws Exception { if (data null || data.trim().isEmpty()) { throw new IllegalArgumentException(加密密码不能为空); } byte[] buf Base64.getDecoder().decode(data); if (buf.length 8) { throw new IllegalArgumentException(加密数据格式不正确); } byte[] head new byte[8]; System.arraycopy(buf, 0, head, 0, head.length); byte[] encryptedData new byte[buf.length - head.length]; System.arraycopy(buf, head.length, encryptedData, 0, encryptedData.length); byte[] decryptedData desDecode(encryptedData, generateKey(head)); return new String(decryptedData); } private static byte[] desDecode(byte[] data, byte[] key) throws Exception { SecureRandom sr new SecureRandom(); DESKeySpec dks new DESKeySpec(key); SecretKeyFactory keyFactory SecretKeyFactory.getInstance(DES); SecretKey securekey keyFactory.generateSecret(dks); Cipher cipher Cipher.getInstance(DES); cipher.init(Cipher.DECRYPT_MODE, securekey, sr); return cipher.doFinal(data); } private static byte[] generateKey(byte[] head) { long ks 3680984568597093857L / (long)(new Random((long)head[5])).nextInt(127); Random random new Random(ks); for(int i 0; i head[0]; i) { random.nextLong(); } long n random.nextLong(); Random r2 new Random(n); long[] ld new long[]{ (long)head[4], r2.nextLong(), (long)head[7], (long)head[3], r2.nextLong(), (long)head[1], random.nextLong(), (long)head[2] }; ByteArrayOutputStream bos new ByteArrayOutputStream(); try (DataOutputStream dos new DataOutputStream(bos)) { for (long l : ld) { dos.writeLong(l); } } catch (IOException e) { throw new RuntimeException(生成密钥失败, e); } return md5(bos.toByteArray()); } private static byte[] md5(byte[] data) { try { MessageDigest md MessageDigest.getInstance(MD5); md.update(data); return md.digest(); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(MD5算法不可用, e); } } }这个改进版的代码主要有以下优化增加了命令行参数处理可以直接在运行时传入加密密码添加了完善的错误处理和输入验证使用了try-with-resources语法确保资源正确释放将一些方法改为private提高了封装性重命名了一些变量和方法使代码更易读5. 实际解密操作步骤有了上面的Java代码接下来就是实际操作环节了。我会详细说明每一步的操作方法准备Java环境确保你的系统安装了JDK 8或以上版本在命令行输入java -version检查是否安装成功保存解密代码将上面的Java代码保存为FinalShellDecodePass.java文件建议保存在一个干净的目录中比如D:\finalShell_decode编译Java程序打开命令行切换到保存java文件的目录执行编译命令javac FinalShellDecodePass.java如果没有报错会生成一个FinalShellDecodePass.class文件执行解密程序在命令行运行java FinalShellDecodePass 你的加密密码将你的加密密码替换为从json文件中复制的password字段值比如java FinalShellDecodePass Pn1vK14tShb4G7ByTjidNtT/EoQ8ic6f获取解密结果程序运行后会直接输出解密后的明文密码如果密码包含特殊字符可能会显示为乱码这时可以尝试指定编码// 修改decodePass方法中的最后一行 return new String(bt, UTF-8);我在实际使用中发现有时候解密出来的密码开头或结尾会多出一些空白字符这是DES加密的填充字符可以直接去掉。另外如果密码中包含中文等非ASCII字符可能需要调整输出编码。6. 常见问题与解决方案在实际操作过程中可能会遇到各种问题。下面是我总结的一些常见问题及解决方法编译错误如果提示javac不是内部或外部命令说明JDK没有正确安装或环境变量没配置解决方案重新安装JDK并配置JAVA_HOME环境变量运行时报错出现InvalidKeyException: Wrong key size错误原因某些JDK版本有强加密限制解决下载安装Java Cryptography Extension (JCE) Unlimited Strength策略文件解密结果不正确可能原因复制的加密密码不完整或有额外字符解决检查password字段是否完整复制确保没有多余的空格或引号找不到conn文件夹可能原因使用了便携版FinalShell或修改过存储路径解决在FinalShell设置中查看或搜索conn文件夹位置多服务器连接如何识别每个json文件都对应一个服务器连接可以按修改时间排序或者打开文件查看其中的host字段确认Linux/Mac系统下的路径Linux: ~/.finalshell/connMac: ~/Library/Application Support/finalshell/conn7. 安全注意事项虽然这个方法可以帮助我们找回遗忘的密码但也要注意相关的安全问题密码存储安全FinalShell的密码加密强度有限不建议长期保存重要服务器的密码对于高敏感服务器最好使用SSH密钥认证方式临时文件清理解密操作完成后记得删除包含密码的java文件和class文件特别是不要在公共电脑上保留这些文件密码使用安全找回密码后建议尽快修改为新的强密码避免在多个服务器使用相同密码程序安全只使用可信的解密代码不要随意运行来历不明的程序我提供的代码可以自行审查不包含任何恶意功能权限管理对于团队环境建议使用专业的密码管理工具避免多人共享同一个FinalShell配置目录8. 其他替代方案除了使用Java程序解密还有其他几种方法可以尝试使用在线解密工具有一些网站提供FinalShell密码解密服务但不推荐使用因为需要上传加密密码存在泄露风险Python实现可以用Python重写解密逻辑优点是无需编译适合没有Java环境的用户直接联系服务器管理员如果是公司服务器最安全的方式还是联系管理员重置密码特别是生产环境服务器不要轻易尝试各种解密方法密码提示功能养成设置密码提示的习惯使用专业的密码管理器如Keepass、Bitwarden等FinalShell备份功能FinalShell有导出连接功能可以备份服务器配置导出时可以选择是否包含密码对于经常需要管理多台服务器的运维人员我建议建立完善的密码管理制度避免依赖客户端的密码保存功能。可以使用Ansible等自动化工具配合SSH证书认证既安全又方便。