V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
0xD800
V2EX  ›  Java

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

  •  
  •   0xD800 · 17 天前 · 4535 次点击

    python 代码如下:

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

    需要用 Java 实现一版,但是发现 java 的 password 参数要传 char[],然后底层转 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 条附言  ·  16 天前

    解决方案如下:

    // 指定一个自定义的 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  
       17 天前
    搜了一下 PBKDF2 的文档: https://www.ietf.org/rfc/rfc2898.txt 其中 password 是一个 string ,那么实现的时候使用 String 对应的 char[]应该是没有问题的。另外既然名称是 password ,使用用户可见的字符来表示也是更合理的。使用字节的密码应该叫做 key 。
    lsk569937453
        2
    lsk569937453  
       17 天前
    java 的类库这么多,找一个符合你要求的就可以了。
    pocketz
        3
    pocketz  
       17 天前
    那么为什么不直接将 byte[] 转为 char[] 呢
    BiChengfei
        4
    BiChengfei  
       17 天前   ❤️ 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
       17 天前
    @orangie 请教下 java 有没有用 key 的类 0.0
    0xD800
        6
    0xD800  
    OP
       17 天前
    @BiChengfei Hex: AC3C90034CF34804A7859144129CA9AEB6B90D07CA874172A374F2000000CAE5 ,麻烦指导一下用 jdk 的 API 算一下 key 出来呗
    0xD800
        7
    0xD800  
    OP
       17 天前
    @lsk569937453 你说得对 0.0
    siweipancc
        8
    siweipancc  
       17 天前 via iPhone
    看得我一脸懵逼,啥跟啥这是
    0xD800
        9
    0xD800  
    OP
       17 天前
    @siweipancc 一个计算微信聊天记录数据库密钥的算法,来源: https://mp.weixin.qq.com/s/4DbXOS5jDjJzM2PN0Mp2JA
    orangie
        10
    orangie  
       17 天前
    @0xD800 按照文档标准设计的应该用字符 password ,不会用字节 key 。PBKDF2 这个名字就是 Password-Based Key Derivation Function 。想要用字节的话,合理的方式不是换个库,而是换个加密方案。如果非要用 PBKDF 系列,那么可以自己把第三方的类库复制一份魔改,注意 license 。
    InkStone
        11
    InkStone  
       17 天前
    这是 password 不是 crypto key 。你传个二进制数据进去本来就不符合用法……

    你要传二进制用普通的 hmac 别用 pbkdf_hmac 啊
    gadfly3173
        12
    gadfly3173  
       17 天前 via Android
    我不懂 python 也不懂 c ,但是按照 python 源码里这个实现,char *也不能用来放非字符吧?
    https://github.com/python/cpython/blob/8b56d82c59c2983b4292a7f506982f2cab352bb2/Modules/_hashopenssl.c#L1323C59-L1323C67
    0xD800
        13
    0xD800  
    OP
       17 天前
    @orangie 真的非常感激你的回复,因为这个是解密微信的本地聊天记录数据库,所以没办法换加密方案,只能换个库或者自己实现这个 SPI 了。
    orangie
        14
    orangie  
       17 天前
    @gadfly3173 C 语言字符和字节的没有区别,C 的 char 完全就是其它语言中的 byte 而没有真正的字符 char 。C 里 char 存的就是字节,完全不限制字符是否合法。
    0xD800
        15
    0xD800  
    OP
       17 天前
    @gadfly3173 感谢回复,python 的 hashlib 中 password 是直接传入 bytes 的,不是传入 string ,因此没有转换的问题。而 java 是传入 UTF8 编码的字符数组,所以出现了问题,JAVA 底层还是把 char 转成了 byte[],我觉得这个设计不太合理,应该支持直接传入 byte[]好一些
    0xD800
        16
    0xD800  
    OP
       17 天前
    @InkStone 微信用的是这个 我也没办法. 我要解密就必须得用 单纯吐槽下这个 API 而已
    0xD800
        17
    0xD800  
    OP
       17 天前
    @orangie 刚刚看了规范里面确实 P 和 S 都是 string ,这么说怪不得 Java 了,只能说其他语言太灵活了。
    ```text
    Input: P password, an octet string
    S salt, an octet string
    ```
    AoEiuV020JP
        18
    AoEiuV020JP  
       17 天前
    以前就听说有一种加密学防破解的手段是使用非标准加密算法,一直没见过实际应用,你这个就是了,
    合理怀疑 python 这个 pbkdf2_hmac 不是因为设计优秀才支持 bytes 的,而是 python 没有 chat[]?
    0xD800
        19
    0xD800  
    OP
       17 天前
    @AoEiuV020JP 哈哈 但是 python 有字符串啊,标准就是传字符串,java 设计出 char[]可能是防止字符串在常量池中,被扫出来吧
    orangie
        20
    orangie  
       17 天前
    @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  
       17 天前
    @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
       17 天前
    @geelaw
    @orangie
    感谢两位指正,我确实没有详细阅读。
    cslive
        23
    cslive  
       17 天前
    不用 string 而用 char[]是为了安全
    xubeiyou
        24
    xubeiyou  
       17 天前   ❤️ 1
    底层是这样的 但是基本都是有对应包装工具类- - 说实话 Java 这么受欢迎还是因为生态好- - 但是价格上不来也 TMD 是因为生态好
    zzl22100048
        25
    zzl22100048  
       17 天前
    从 #1 给的文档点进去,password 是 octet string ,也是就 bytes
    也就是 java 库没按规范实现
    yippees
        26
    yippees  
       17 天前   ❤️ 1
    s = "48656c6c6f20576f726c64"
    b = bytes.fromhex(s)
    print(b)
    b'Hello World'

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

    主题的错误实现代码 回复的歪楼 看得真是太欢乐了,,,
    都想借用楼主的标题了,,,
    miaotaizi
        27
    miaotaizi  
       17 天前
    你就不能味给 AI 让 AI 帮你实现一版 JAVA 的吗
    zzl22100048
        28
    zzl22100048  
       17 天前
    @yippees
    op 说的是 sun 实现的代码问题
    PBKDF2 强制用 utf8 做 password
    0xD800
        29
    0xD800  
    OP
       17 天前
    @yippees 其实是你没看懂我上面的代码哦
    0xD800
        30
    0xD800  
    OP
       17 天前
    @miaotaizi 上面的翻译代码其实是 AI 给我的,我 debug 看了,改动不大我都准备自己改下了
    geelaw
        31
    geelaw  
       17 天前 via iPhone
    @BiChengfei #4 我觉得“糟糕”“无语”都还好吧,“屁”这个应该是楼主的错字,原文“个屁”实际上应该是 gěrpì(常写作:嗝儿屁),语气和意思差不多都是“死翘翘”。
    0xD800
        32
    0xD800  
    OP
       17 天前
    @zzl22100048 是的👍
    0xD800
        33
    0xD800  
    OP
       17 天前
    @geelaw 是的,输入法的问题,我想打的是嗝屁了
    pkoukk
        34
    pkoukk  
       17 天前
    吐槽一个 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  
       17 天前
    @BiChengfei 个屁(嗝屁)是个地方方言,意思是完蛋了,这个不是粗口,可以暂缓 block
    0xD800
        36
    0xD800  
    OP
       17 天前
    @pkoukk #34 哈哈 挺无语的
    yusheng88
        37
    yusheng88  
       17 天前 via Android
    对一个东西不懂时,最好保持谦虚学习态度。

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

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

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

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

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

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

    填充算法也简单。
    DefoliationM
        43
    DefoliationM  
       16 天前 via Android
    转成 base64 转一下。。
    0xD800
        44
    0xD800  
    OP
       16 天前
    @DefoliationM #43 很遗憾 不行的。。。
    0xD800
        45
    0xD800  
    OP
       16 天前
    解决方案如下:

    // 指定一个自定义的 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 ,搜了下好像是清理用的,我没处理这个直接注释了。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1263 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 28ms · UTC 18:04 · PVG 02:04 · LAX 11:04 · JFK 14:04
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.