分享一个 Java 中非常糟糕的 API 设计 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
0xD800
V2EX    Java

分享一个 Java 中非常糟糕的 API 设计

  •  
  •   0xD800 2024-04-30 10:15:10 +08:00 5824 次点击
    这是一个创建于 536 天前的主题,其中的信息可能已经有所发展或是发生改变。

    python 代码如下:

    hashlib.pbkdf2_hmac('sha1', bytes.fromhex('******'), bytes.fromhex('00000000000000000000000000000000'), 64000, 32) 

    需要用 Java 实现一版,但是发现 java 的 password 参数要传 chr[],然后底层转 bytes ,代码如下:

    // com.sun.crypto.provider.PBKDF2KeyImpl#getPasswordBytes private static byte[] getPasswordBytes(char[] passwd) { CharBuffer cb = CharBuffer.wrap(passwd); ByteBuffer bb = UTF_8.encode(cb); int len = bb.limit(); byte[] passwdBytes = new byte[len]; bb.get(passwdBytes, 0, len); bb.clear().put(new byte[len]); return passwdBytes; } 

    真无语了,这么写相当于密码只能用字符串转 char[]了,不能用二进制的 password ,如果 password 是非法字符序列就个屁了。

    /** * hashlib.pbkdf2_hmac('sha1', password, salt, iterations, key_length) */ private static byte[] generateKey(byte[] password, byte[] salt, int iterationCount, int keyLength) throws Exception { // 由于 password 非字符序列导致 new String 后数据失真,底层无法还原会原始 bytes 。 char[] encoded = new String(password, StandardCharsets.UTF_8).toCharArray(); // 创建密钥规范 KeySpec spec = new PBEKeySpec(encoded, salt, iterationCount, keyLength * 8); // 使用 PBKDF2WithHmacSHA1 算法创建密钥工厂 SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); // 生成密钥 SecretKey secretKey = factory.generateSecret(spec); return secretKey.getEncoded(); } 

    这是一个微信聊天记录数据库算法。。

    第 1 条附言    2024-05-02 00:09:47 +08:00

    解决方案如下:

    // 指定一个自定义的 Provider

    SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1", new PBEProvider()); 
    // PBEProvider public class PBEProvider extends Provider { public PBEProvider() { super("PBEProvider", 1.0, "MyProvider v1.0: Custom SecretKeyFactorySpi Implementation"); put("SecretKeyFactory.PBKDF2WithHmacSHA1", PBESecretKeyFactorySpi.class.getName()); } } 
    // PBESecretKeyFactorySpi public class PBESecretKeyFactorySpi extends SecretKeyFactorySpi { String prfAlgo = "HmacSHA1"; @Override protected SecretKey engineGenerateSecret(KeySpec spec) throws InvalidKeySpecException { if (spec instanceof PBEKeySpec pksp) { return new PBKDF2KeyImpl(pksp, this.prfAlgo); } else { throw new InvalidKeySpecException("Unsupported KeySpec"); } } // ...省略其他方法 } 
    // 自己实现一个 PBKDF2KeyImpl // 重写 getPasswdBytes 方法 // 将每个 byte 直接转成 char 传入,然后再强转还原 byte[]即可 private static byte[] getPasswordBytes(char[] passwd) { byte[] result = new byte[passwd.length]; for (int i = 0; i < passwd.length; i++) { result[i] = (byte) passwd[i]; } return result; } 

    其中遇到一个问题,JDK 的 PBKDF2KeyImpl 里面有 CleanFactory ,搜了下好像是清理用的,我没处理这个直接注释了。

    45 条回复    2024-05-02 00:07:26 +08:00
    orangie
        1
    orangie  
       2024-04-30 10:24:35 +08:00
    搜了一下 PBKDF2 的文档: https://www.ietf.org/rfc/rfc2898.txt 其中 password 是一个 string ,那么实现的时候使用 String 对应的 char[]应该是没有问题的。另外既然名称是 password ,使用用户可见的字符来表示也是更合理的。使用字节的密码应该叫做 key 。
    lsk569937453
        2
    lsk569937453  
       2024-04-30 10:29:15 +08:00
    java 的类库这么多,找一个符合你要求的就可以了。
    pocketz
        3
    pocketz  
       2024-04-30 10:35:11 +08:00
    那么为什么不直接将 byte[] 转为 char[] 呢
    BiChengfei
        4
    BiChengfei  
       2024-04-30 10:40:40 +08:00   2
    hashlib.pbkdf2_hmac('sha1', password, salt, iterations, key_length)
    我理解你是想使用 sha1 算法对 password 进行 hash 运算,salt 表示加盐,iterations 表示计算次数,key_length 表示 hash 后的长度
    非法字符序列,举个例子啊,是指不在 UTF_8 中的字符吗,上来就是”糟糕“、”无语“、”屁“,已 block
    0xD800
        5
    0xD800  
    OP
       2024-04-30 10:41:07 +08:00
    @orangie 请教下 java 有没有用 key 的类 0.0
    0xD800
        6
    0xD800  
    OP
       2024-04-30 10:42:42 +08:00
    @BiChengfei Hex: AC3C90034CF34804A7859144129CA9AEB6B90D07CA874172A374F2000000CAE5 ,麻烦指导一下用 jdk 的 API 算一下 key 出来呗
    0xD800
        7
    0xD800  
    OP
       2024-04-30 10:42:55 +08:00
    @lsk569937453 你说得对 0.0
    siweipancc
        8
    siweipancc  
       2024-04-30 10:43:00 +08:00 via iPhone
    看得我一脸懵逼,啥跟啥这是
    0xD800
        9
    0xD800  
    OP
       2024-04-30 10:45:42 +08:00
    @siweipancc 一个计算微信聊天记录数据库密钥的算法,来源: https://mp.weixin.qq.com/s/4DbXOS5jDjJzM2PN0Mp2JA
    orangie
        10
    orangie  
       2024-04-30 10:48:08 +08:00
    @0xD800 按照文档标准设计的应该用字符 password ,不会用字节 key 。PBKDF2 这个名字就是 Password-Based Key Derivation Function 。想要用字节的话,合理的方式不是换个库,而是换个加密方案。如果非要用 PBKDF 系列,那么可以自己把第三方的类库复制一份魔改,注意 license 。
    InkStone
        11
    InkStone  
       2024-04-30 10:49:29 +08:00
    这是 password 不是 crypto key 。你传个二进制数据进去本来就不符合用法……

    你要传二进制用普通的 hmac 别用 pbkdf_hmac 啊
    gadfly3173
        12
    gadfly3173  
       2024-04-30 10:50:00 +08:00 via Android
    我不懂 python 也不懂 c ,但是按照 python 源码里这个实现,char *也不能用来放非字符吧?
    https://github.com/python/cpython/blob/8b56d82c59c2983b4292a7f506982f2cab352bb2/Modules/_hashopenssl.c#L1323C59-L1323C67
    0xD800
        13
    0xD800  
    OP
       2024-04-30 10:53:23 +08:00
    @orangie 真的非常感激你的回复,因为这个是解密微信的本地聊天记录数据库,所以没办法换加密方案,只能换个库或者自己实现这个 SPI 了。
    orangie
        14
    orangie  
       2024-04-30 10:54:42 +08:00
    @gadfly3173 C 语言字符和字节的没有区别,C 的 char 完全就是其它语言中的 byte 而没有真正的字符 char 。C 里 char 存的就是字节,完全不限制字符是否合法。
    0xD800
        15
    0xD800  
    OP
       2024-04-30 10:55:09 +08:00
    @gadfly3173 感谢回复,python 的 hashlib 中 password 是直接传入 bytes 的,不是传入 string ,因此没有转换的问题。而 java 是传入 UTF8 编码的字符数组,所以出现了问题,JAVA 底层还是把 char 转成了 byte[],我觉得这个设计不太合理,应该支持直接传入 byte[]好一些
    0xD800
        16
    0xD800  
    OP
       2024-04-30 10:55:48 +08:00
    @InkStone 微信用的是这个 我也没办法. 我要解密就必须得用 单纯吐槽下这个 API 而已
    0xD800
        17
    0xD800  
    OP
       2024-04-30 11:06:29 +08:00
    @orangie 刚刚看了规范里面确实 P 和 S 都是 string ,这么说怪不得 Java 了,只能说其他语言太灵活了。
    ```text
    Input: P password, an octet string
    S salt, an octet string
    ```
    AoEiuV020JP
        18
    AoEiuV020JP  
       2024-04-30 11:07:00 +08:00
    以前就听说有一种加密学防破解的手段是使用非标准加密算法,一直没见过实际应用,你这个就是了,
    合理怀疑 python 这个 pbkdf2_hmac 不是因为设计优秀才支持 bytes 的,而是 python 没有 chat[]?
    0xD800
        19
    0xD800  
    OP
       2024-04-30 11:12:32 +08:00
    @AoEiuV020JP 哈哈 但是 python 有字符串啊,标准就是传字符串,java 设计出 char[]可能是防止字符串在常量池中,被扫出来吧
    orangie
        20
    orangie  
       2024-04-30 11:14:40 +08:00
    @0xD800 不对。这里说的 octet string 就是字节串的意思,而文档中的如下段落才是真正使用 char[]的原因:

    Throughout this document, a password is considered to be an octet
    string of arbitrary length whose interpretation as a text string is
    unspecified. In the interest of interoperability, however, it is
    recommended that applications follow some common text encoding rules.
    ASCII and UTF-8 [27] are two possibilities. (ASCII is a subset of
    UTF-8.)

    这个 password 是字节串,但是仍然推荐使用符合 ASCII 或者 utf-8 等编码的兼容的表示方法来保持兼容性。我也是现学的。
    geelaw
        21
    geelaw  
       2024-04-30 11:14:51 +08:00
    @orangie #1 找到之后还需要认真阅读,这个 RFC 里面说的是 (p. 4)

    Throughout this document, a password is considered to be an octet
    string of arbitrary length whose interpretation as a text string is
    unspecified. In the interest of interoperability, however, it is
    recommended that applications follow some common text encoding rules.
    ASCII and UTF-8 [27] are two possibilities. (ASCII is a subset of
    UTF-8.)

    并且 (p. 9)

    Input: P password, an octet string

    文档里没有定义什么是 octet string ,自然的理解是指 byte string ,即字节组成的序列。

    一般编程概念里 string 也不一定非要是 text string ,单纯是指某个枚举类型(比如 byte 、char 、uint32 之类的)的序列罢了。

    @0xD800 #17 这是误读。
    0xD800
        22
    0xD800  
    OP
       2024-04-30 11:19:41 +08:00
    @geelaw
    @orangie
    感谢两位指正,我确实没有详细阅读。
    cslive
        23
    cslive  
       2024-04-30 11:32:49 +08:00
    不用 string 而用 char[]是为了安全
    xubeiyou
        24
    xubeiyou  
       2024-04-30 11:36:44 +08:00   1
    底层是这样的 但是基本都是有对应包装工具类- - 说实话 Java 这么受欢迎还是因为生态好- - 但是价格上不来也 TMD 是因为生态好
    zzl22100048
        25
    zzl22100048  
       2024-04-30 15:23:35 +08:00
    从 #1 给的文档点进去,password 是 octet string ,也是就 bytes
    也就是 java 库没按规范实现
    yippees
        26
    yippees  
       2024-04-30 15:46:38 +08:00   1
    s = "48656c6c6f20576f726c64"
    b = bytes.fromhex(s)
    print(b)
    b'Hello World'

    顾名思义不是一个基本的 hex-str 转 byte 数组吗。。。。48-》 0X48 。。。

    主题的错误实现代码 回复的歪楼 看得真是太欢乐了,,,
    都想借用楼主的标题了,,,
    miaotaizi
        27
    miaotaizi  
       2024-04-30 15:50:23 +08:00
    你就不能味给 AI 让 AI 帮你实现一版 JAVA 的吗
    zzl22100048
        28
    zzl22100048  
       2024-04-30 17:13:38 +08:00
    @yippees
    op 说的是 sun 实现的代码问题
    PBKDF2 强制用 utf8 做 password
    0xD800
        29
    0xD800  
    OP
       2024-04-30 17:21:01 +08:00
    @yippees 其实是你没看懂我上面的代码哦
    0xD800
        30
    0xD800  
    OP
       2024-04-30 17:21:31 +08:00
    @miaotaizi 上面的翻译代码其实是 AI 给我的,我 debug 看了,改动不大我都准备自己改下了
    geelaw
        31
    geelaw  
       2024-04-30 17:21:46 +08:00 via iPhone
    @BiChengfei #4 我觉得“糟糕”“无语”都还好吧,“屁”这个应该是楼主的错字,原文“个屁”实际上应该是 gěrpì(常写作:嗝儿屁),语气和意思差不多都是“死翘翘”。
    0xD800
        32
    0xD800  
    OP
       2024-04-30 17:26:36 +08:00
    @zzl22100048 是的
    0xD800
        33
    0xD800  
    OP
       2024-04-30 17:27:24 +08:00
    @geelaw 是的,输入法的问题,我想打的是嗝屁了
    pkoukk
        34
    pkoukk  
       2024-04-30 18:34:22 +08:00
    吐槽一个 java 更离谱的 API javax.crypto.spec.DESKeySpec(byte[] key)
    Creates a DESKeySpec object using the first 8 bytes in key as the key material for the DES key.

    当年对接一个 java 服务的 API ,他让我用 des 签名,然后给了我一个 16 位字符串的 key
    我瞬间小脑萎缩了?你这 key 怎么放进去的?
    然后他给了我 java 的 demo ,我才发现是标准库干的骚操作,我头上一万个问号,至今想不通为什么。
    yankebupt
        35
    yankebupt  
       2024-04-30 18:53:11 +08:00
    @BiChengfei 个屁(嗝屁)是个地方方言,意思是完蛋了,这个不是粗口,可以暂缓 block
    0xD800
        36
    0xD800  
    OP
       2024-04-30 19:48:57 +08:00
    @pkoukk #34 哈哈 挺无语的
    a5X77vajGRyLA2aF
        37
    a5X77vajGRyLA2aF  
       2024-04-30 20:36:44 +08:00 via Android
    对一个东西不懂时,最好保持谦虚学习态度。

    python 类库底层怎么处理的你不看
    PBKDF 定义你不看
    jdk 的类库你不研究

    ai 翻译的不合你的"以为",你又要喷。
    通篇下来就凸显浮躁和无脑,没有现成类库喂饭到嘴就啥也不是。
    0o0O0o0O0o
        38
    0o0O0o0O0o  
       2024-04-30 20:50:25 +08:00
    无法评判 sun 的这个库到底实现得有没有问题,但逆向移植会经常遇到这类不一致,一般是找到的偏移也许并不对应应用开发人员直接写的代码,可能只是应用所用到的加密库里的某个环节,或者开发人员真的误用、魔改。

    而且流行的加密库往往都搞一些密码学的最佳实践,加一些默认设置或者屏蔽掉一些功能,已经习惯了此路不通就立刻找一份实现改改,不死磕。

    - https://stackoverflow.com/a/35536933
    - https://stackoverflow.com/a/51230724
    Rache1
        39
    Rache1  
       2024-04-30 21:14:36 +08:00
    @pkoukk #34 有时候人的问题也占很大一部分,Java 有个工具库 hutool ,有人写接口或者加解密就用里面的 API 一把梭。

    然后给文档的时候就说 AES/RSA 加密的,给个密钥。一问 IV 、具体算法、填充方式就抓瞎了……
    0xD800
        40
    0xD800  
    OP
       2024-05-01 00:32:06 +08:00
    @0o0O0o0O0o 哈哈 我并不是死磕,而是翻译代码的时候发现这个设计很奇葩,居然只允许用 char[],我用过其他的加密库都是允许传 byte[],这个操作我确实无法理解。
    0xD800
        41
    0xD800  
    OP
       2024-05-01 00:36:05 +08:00
    @yusheng88 #37
    回复:
    1. 上面有朋友发了 CPython 的实现,password 是允许字节流的
    2. PBKDF 定义没看,但是可以参考#21 的回复,规范定义是字节流,只是建议用 ASCII 或 UTF8 序列
    3. JDK 的类库我是研究了才发现这个奇葩的设计的呢

    所以您有什么更好的解决方案吗?请指教。
    另外我英文水平不是很好,无法直接阅读上面那些规范,自然不愿意去细读,那个网页的排版也差。
    0xD800
        42
    0xD800  
    OP
       2024-05-01 00:37:32 +08:00
    @Rache1 说到这个我其实还是会点,至少对填充方式,一些数论基础,RSA 加解密原理,ECC 加解密原理都是熟悉的。
    不过 IV 之类的了解还挺少,用的不多,我觉得也不难吧。

    填充算法也简单。
    DefoliationM
        43
    DefoliationM  
       2024-05-01 12:57:10 +08:00 via Android
    转成 base64 转一下。。
    0xD800
        44
    0xD800  
    OP
       2024-05-01 15:20:33 +08:00
    @DefoliationM #43 很遗憾 不行的。。。
    0xD800
        45
    0xD800  
    OP
       2024-05-02 00:07:26 +08:00
    解决方案如下:

    // 指定一个自定义的 Provider
    SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1", new PBEProvider());

    // PBEProvider
    public class PBEProvider extends Provider {
    public PBEProvider() {
    super("PBEProvider", 1.0, "MyProvider v1.0: Custom SecretKeyFactorySpi Implementation");
    put("SecretKeyFactory.PBKDF2WithHmacSHA1", PBESecretKeyFactorySpi.class.getName());
    }
    }

    // PBESecretKeyFactorySpi
    public class PBESecretKeyFactorySpi extends SecretKeyFactorySpi {

    tring prfAlgo = "HmacSHA1";

    @Override
    protected SecretKey engineGenerateSecret(KeySpec spec) throws InvalidKeySpecException {
    if (spec instanceof PBEKeySpec pksp) {
    return new PBKDF2KeyImpl(pksp, this.prfAlgo);
    } else {
    throw new InvalidKeySpecException("Unsupported KeySpec");
    }
    }
    // ...省略其他方法
    }


    // 自己实现一个 PBKDF2KeyImpl
    // 重写 getPasswdBytes 方法
    // 将每个 byte 直接转成 char 传入,然后再强转还原 byte[]即可
    private static byte[] getPasswordBytes(char[] passwd) {
    byte[] result = new byte[passwd.length];

    for (int i = 0; i < passwd.length; i++) {
    result[i] = (byte) passwd[i];
    }

    return result;
    }


    其中遇到一个问题,JDK 的 PBKDF2KeyImpl 里面有 CleanFactory ,搜了下好像是清理用的,我没处理这个直接注释了。
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     877 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 31ms UTC 21:20 PVG 05:20 LAX 14:20 JFK 17:20
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86