【问题标题】:Handling passwords used for auth in source code处理源代码中用于身份验证的密码
【发布时间】:2012-10-07 21:46:17
【问题描述】:

假设我正在尝试从使用基本身份验证/基本证书的 RESTful api 中提取,那么在我的程序中存储该用户名和密码的最佳方式是什么?现在它只是以纯文本形式存在。

UsernamePasswordCredentials creds = new UsernamePasswordCredentials("myName@myserver","myPassword1234");

有没有更安全的方法?

谢谢

【问题讨论】:

  • 答案取决于以下几点:您要分发应用程序吗?用户/密码是基于应用程序的用户,还是某种 API 密钥?你想保护本地用户(某种 DRM)的用户/密码吗?
  • 它实际上是一个在后端运行的程序,但它实际上更多的是关于样式。我不应该拥有以明文形式存储机密级别信息的帐户的用户名/密码。
  • 看看这个帖子stackoverflow.com/questions/12198228/…,你就会得到大致的想法。

标签: java security authentication


【解决方案1】:

重要提示:

如果您将身份验证系统作为一个整体进行设计,则不应存储密码,即使密码已加密。您存储一个哈希,并检查登录期间提供的密码是否与相同的哈希匹配。这样一来,您的数据库中的安全漏洞就可以避免暴露您的用户密码。

话虽如此,对于您要按原样存储数据(在本例中为密码)的情况,然后采用从内到外的心态,以下是保护您的流程的一些步骤:


第一步,您应该将密码处理从String 更改为character array。

原因是String是immutable对象,所以即使对象设置为null,它的数据也不会立即被清理;数据被设置为垃圾收集,这会带来安全问题,因为恶意程序可能会在清理之前访问该String(密码)数据。

这是Swing's JPasswordField's getText()方法被弃用的主要原因,也是getPassword() uses character arrays的主要原因。


第二步是加密您的凭据,仅在身份验证过程中临时解密它们。或者在服务器端对它们进行散列,存储该散列,然后“忘记”原始密码。

这与第一步类似,可确保您的漏洞时间尽可能短。

建议您的凭据不要硬编码,而是以集中、可配置且易于维护的方式存储它们,例如配置或属性文件或数据库。

您应该在保存文件之前加密您的凭据,此外,您可以对文件本身应用第二次加密(对凭据进行 2 层加密,对其他文件内容进行 1 层加密)。

请注意,上面提到的两个加密过程中的每一个都可以是多层的。作为概念示例,每个加密都可以是 Triple Data Encryption Standard (AKA TDES and 3DES) 的单独应用程序。


在您的本地环境得到适当保护后(但请记住,它永远不会“安全”!),第三步是使用TLS (Transport Layer Security) or SSL (Secure Sockets Layer) 为您的传输过程应用基本保护。


第四步是应用其他保护方法。

例如,对您的“使用”编译应用混淆技术,以避免(即使很快)暴露您的安全措施,以防您的程序被Ms. Eve, Mr. Mallory, or someone else (the bad-guys) 获取并被反编译。


更新 1:

应@Damien.Bell 的要求,下面是一个涵盖第一步和第二步的示例:

    //These will be used as the source of the configuration file's stored attributes.
    private static final Map<String, String> COMMON_ATTRIBUTES = new HashMap<String, String>();
    private static final Map<String, char[]> SECURE_ATTRIBUTES = new HashMap<String, char[]>();
    //Ciphering (encryption and decryption) password/key.
    private static final char[] PASSWORD = "Unauthorized_Personel_Is_Unauthorized".toCharArray();
    //Cipher salt.
    private static final byte[] SALT = {
        (byte) 0xde, (byte) 0x33, (byte) 0x10, (byte) 0x12,
        (byte) 0xde, (byte) 0x33, (byte) 0x10, (byte) 0x12,};
    //Desktop dir:
    private static final File DESKTOP = new File(System.getProperty("user.home") + "/Desktop");
    //File names:
    private static final String NO_ENCRYPTION = "no_layers.txt";
    private static final String SINGLE_LAYER = "single_layer.txt";
    private static final String DOUBLE_LAYER = "double_layer.txt";

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) throws GeneralSecurityException, FileNotFoundException, IOException {
        //Set common attributes.
        COMMON_ATTRIBUTES.put("Gender", "Male");
        COMMON_ATTRIBUTES.put("Age", "21");
        COMMON_ATTRIBUTES.put("Name", "Hypot Hetical");
        COMMON_ATTRIBUTES.put("Nickname", "HH");

        /*
         * Set secure attributes.
         * NOTE: Ignore the use of Strings here, it's being used for convenience only.
         * In real implementations, JPasswordField.getPassword() would send the arrays directly.
         */
        SECURE_ATTRIBUTES.put("Username", "Hypothetical".toCharArray());
        SECURE_ATTRIBUTES.put("Password", "LetMePass_Word".toCharArray());

        /*
         * For demosntration purposes, I make the three encryption layer-levels I mention.
         * To leave no doubt the code works, I use real file IO.
         */
        //File without encryption.
        create_EncryptedFile(NO_ENCRYPTION, COMMON_ATTRIBUTES, SECURE_ATTRIBUTES, 0);
        //File with encryption to secure attributes only.
        create_EncryptedFile(SINGLE_LAYER, COMMON_ATTRIBUTES, SECURE_ATTRIBUTES, 1);
        //File completely encrypted, including re-encryption of secure attributes.
        create_EncryptedFile(DOUBLE_LAYER, COMMON_ATTRIBUTES, SECURE_ATTRIBUTES, 2);

        /*
         * Show contents of all three encryption levels, from file.
         */
        System.out.println("NO ENCRYPTION: \n" + readFile_NoDecryption(NO_ENCRYPTION) + "\n\n\n");
        System.out.println("SINGLE LAYER ENCRYPTION: \n" + readFile_NoDecryption(SINGLE_LAYER) + "\n\n\n");
        System.out.println("DOUBLE LAYER ENCRYPTION: \n" + readFile_NoDecryption(DOUBLE_LAYER) + "\n\n\n");

        /*
         * Decryption is demonstrated with the Double-Layer encryption file.
         */
        //Descrypt first layer. (file content) (REMEMBER: Layers are in reverse order from writing).
        String decryptedContent = readFile_ApplyDecryption(DOUBLE_LAYER);
        System.out.println("READ: [first layer decrypted]\n" + decryptedContent + "\n\n\n");
        //Decrypt second layer (secure data).
        for (String line : decryptedContent.split("\n")) {
            String[] pair = line.split(": ", 2);
            if (pair[0].equalsIgnoreCase("Username") || pair[0].equalsIgnoreCase("Password")) {
                System.out.println("Decrypted: " + pair[0] + ": " + decrypt(pair[1]));
            }
        }
    }

    private static String encrypt(byte[] property) throws GeneralSecurityException {
        SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBEWithMD5AndDES");
        SecretKey key = keyFactory.generateSecret(new PBEKeySpec(PASSWORD));
        Cipher pbeCipher = Cipher.getInstance("PBEWithMD5AndDES");
        pbeCipher.init(Cipher.ENCRYPT_MODE, key, new PBEParameterSpec(SALT, 20));

        //Encrypt and save to temporary storage.
        String encrypted = Base64.encodeBytes(pbeCipher.doFinal(property));

        //Cleanup data-sources - Leave no traces behind.
        for (int i = 0; i < property.length; i++) {
            property[i] = 0;
        }
        property = null;
        System.gc();

        //Return encryption result.
        return encrypted;
    }

    private static String encrypt(char[] property) throws GeneralSecurityException {
        //Prepare and encrypt.
        byte[] bytes = new byte[property.length];
        for (int i = 0; i < property.length; i++) {
            bytes[i] = (byte) property[i];
        }
        String encrypted = encrypt(bytes);

        /*
         * Cleanup property here. (child data-source 'bytes' is cleaned inside 'encrypt(byte[])').
         * It's not being done because the sources are being used multiple times for the different layer samples.
         */
//      for (int i = 0; i < property.length; i++) { //cleanup allocated data.
//          property[i] = 0;
//      }
//      property = null; //de-allocate data (set for GC).
//      System.gc(); //Attempt triggering garbage-collection.

        return encrypted;
    }

    private static String encrypt(String property) throws GeneralSecurityException {
        String encrypted = encrypt(property.getBytes());
        /*
         * Strings can't really have their allocated data cleaned before CG,
         * that's why secure data should be handled with char[] or byte[].
         * Still, don't forget to set for GC, even for data of sesser importancy;
         * You are making everything safer still, and freeing up memory as bonus.
         */
        property = null;
        return encrypted;
    }

    private static String decrypt(String property) throws GeneralSecurityException, IOException {
        SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBEWithMD5AndDES");
        SecretKey key = keyFactory.generateSecret(new PBEKeySpec(PASSWORD));
        Cipher pbeCipher = Cipher.getInstance("PBEWithMD5AndDES");
        pbeCipher.init(Cipher.DECRYPT_MODE, key, new PBEParameterSpec(SALT, 20));
        return new String(pbeCipher.doFinal(Base64.decode(property)));
    }

    private static void create_EncryptedFile(
                    String fileName,
                    Map<String, String> commonAttributes,
                    Map<String, char[]> secureAttributes,
                    int layers)
                    throws GeneralSecurityException, FileNotFoundException, IOException {
        StringBuilder sb = new StringBuilder();
        for (String k : commonAttributes.keySet()) {
            sb.append(k).append(": ").append(commonAttributes.get(k)).append(System.lineSeparator());
        }
        //First encryption layer. Encrypts secure attribute values only.
        for (String k : secureAttributes.keySet()) {
            String encryptedValue;
            if (layers >= 1) {
                encryptedValue = encrypt(secureAttributes.get(k));
            } else {
                encryptedValue = new String(secureAttributes.get(k));
            }
            sb.append(k).append(": ").append(encryptedValue).append(System.lineSeparator());
        }

        //Prepare file and file-writing process.
        File f = new File(DESKTOP, fileName);
        if (!f.getParentFile().exists()) {
            f.getParentFile().mkdirs();
        } else if (f.exists()) {
            f.delete();
        }
        BufferedWriter bw = new BufferedWriter(new FileWriter(f));
        //Second encryption layer. Encrypts whole file content including previously encrypted stuff.
        if (layers >= 2) {
            bw.append(encrypt(sb.toString().trim()));
        } else {
            bw.append(sb.toString().trim());
        }
        bw.flush();
        bw.close();
    }

    private static String readFile_NoDecryption(String fileName) throws FileNotFoundException, IOException, GeneralSecurityException {
        File f = new File(DESKTOP, fileName);
        BufferedReader br = new BufferedReader(new FileReader(f));
        StringBuilder sb = new StringBuilder();
        while (br.ready()) {
            sb.append(br.readLine()).append(System.lineSeparator());
        }
        return sb.toString();
    }

    private static String readFile_ApplyDecryption(String fileName) throws FileNotFoundException, IOException, GeneralSecurityException {
        File f = new File(DESKTOP, fileName);
        BufferedReader br = new BufferedReader(new FileReader(f));
        StringBuilder sb = new StringBuilder();
        while (br.ready()) {
            sb.append(br.readLine()).append(System.lineSeparator());
        }
        return decrypt(sb.toString());
    }

解决每个保护步骤的完整示例将远远超出我认为对这个问题的合理性,因为它是关于“步骤是什么”,而不是“如何应用它们".

这将远远超过我的答案(最后是抽样),而 S.O.已经针对这些步骤的“如何做”进行了说明,更加合适,并为每个单独步骤的实施提供了更好的解释和示例。

【讨论】:

  • [*] - @Damien.Bell 为了不让您的请求无人看管,我提供了一个涵盖第一步 (~) 和第二步的示例。 --- 至于为什么不是所有的步骤,好吧,正如你所看到的,这不是你可以用一点点 sn-p 代码来采样的东西;一个网络保护的例子甚至需要比本地范围的更多,即使是部分伪编码。混淆也有非常广泛的实现方法,虽然它的概念很简单,但它应用于源代码本身意味着它很难在示例中解释。
  • 最后,在您的源代码上运行像 ProGuard 这样的混淆工具。众所周知,Java 字节码易于反汇编和分析。混淆是您的安全蛋糕上的糖衣,它使某人更难对您的代码进行逆向工程并可能破解您的安全措施。见:proguard.sourceforge.net/index.html#manual/introduction.html
  • @Woot4Moo - 据我了解,由于他试图从非本地实体(服务器)中提取,因此范围不是从认证者(服务器)的角度来看,但从客户端的角度来看,是认证过程。 --- 因此,客户端必须在存储和传输中保护凭证,但按原样发送凭证。传输安全由第三步处理,它固有地具有加密、散列和消息消化。 ___ 服务器是此类哈希比较适用的地方,出于安全原因,客户端不应执行服务器任务。
  • @Roland 在过去一年左右的时间里我没有用 Java 编写任何东西,所以我不记得有什么关于 CharBuffer 的特别想法,但基本上,如果它不是不可变的(可以得到它的内部数据覆盖到null或零而不等待GC),然后你可以使用它,只要你不忘记清理它。
  • 在我的源代码中拥有所有的解密和加密信息(包括盐),这如何更安全?
【解决方案2】:

为什么不在源代码中存储凭据

避免在源代码中存储凭据通常是个好主意。 问题是,对代码的访问以及谁应该有权访问凭据通常会随着时间的推移而变化。一旦项目变得更加成熟,通常会有一些开发人员不需要知道,因此不应该知道某些凭据。此外,代码可能会被重用于稍有不同的目的,甚至成为开源代码。此外,随着代码库变得越来越复杂,识别隐藏在代码中间某处的凭据变得非常繁琐。

可以肯定地说,数以亿计的用户已经受到硬编码凭据引起的问题的影响。 Here is an article with some examples.

如何为您的应用提供凭据

如果凭据不是代码的一部分,这会引发如何向应用程序提供凭据的问题。这取决于您的应用程序运行的平台。例如,如果您在某个云服务上托管您的应用程序,该服务将有一种机制以保存方式存储凭据并将它们注入您的应用程序的操作系统环境。为了提供一个具体的例子,这里是文档how to provide credentials for an app hosted on Heroku。 在您的应用程序代码中,您可以从环境中访问它们。例如。对于 Java,您可以使用 getenv

String apiPassword = getenv("API_PASSWORD");

这里API_PASSWORD需要你的应用的宿主机制在环境中提供。

进一步阅读

我写了一篇关于该主题的博客文章,更详细地介绍了该主题:Keep passwords out of source code - why and how。

【讨论】:

    【解决方案3】:

    为什么人们在谈论散列。 OP 希望存储他的用户凭据以访问外部资源。散列他的密码也无济于事。

    现在不碍事了。我只想为每一层提供简单的最佳实践。

    1 .将您的密码存储在 java 应用程序中。 :将其存储为字符数组。创建一个密码存储类并将密码存储为哈希图,其中键作为您要访问的资源,并将值作为包含用户名和密码的某个对象。使用一些身份验证限制此 api 的入口点 例如:接受登录用户的凭据以验证该用户对该资源的访问级别(只需将用户映射到他们可以访问的密码列表。如果你有很多创建一个组并将密码映射键映射到该组)除此之外存储密码的任何内容都取决于您对 jvm 本身泄漏它的偏执程度。

    1. 要传输密码,请确保您在安全的端口上发送密码(例如:Https 是好的,http 是坏的)。如果您真的必须通过不安全的协议进行传输,请对其进行加密并将其编码为 base64。确保收件人解码并可以解密您的密码。

    【讨论】:

    • OP 存在存储凭据的情况,但这不是问题所在。 OP专门询问安全性。 => “有没有更安全的方法?”
    • 我不明白你的评论是什么意思。 OP 带着一种情况来到这里,如果这就是你所指的,你不能断章取意。尽管如此,我还是提供了简单而有效的方法来保护在 JVM 中和传输时对密码的访问。我可以帮助澄清细节,但我认为没有必要这样做,因为问题是关于方法而不是实施。此外,无论您的安全意识如何,您都无法散列和存储您希望用于访问第三方网站的密码。
    • 我没有断章取意。问题是您似乎不了解 OP,或者一般来说这个网站:他向您提出了一个具体问题。你应该给他一个关于这个问题的答案。 - - 还。将密码作为类存储在应用程序中,您实际上是将密码放入源代码中。这是存储密码的最糟糕的解决方案之一。如果你不是在谈论那个,那么你需要一个文件,这意味着明文
    【解决方案4】:
    1. 初始化请求的安全计算机(您的计算机)。如果那台机器不安全,没有任何东西可以保护你。这是完全独立的主题(最新软件、正确配置、强密码、加密交换、硬件嗅探器、物理安全等)
    2. 保护您的存储 您用于存储凭据的介质应加密。解密的凭据应仅存储在受保护机器的 ram 中
    3. 必须信任维护该硬件的人员(可能是最薄弱的环节)
    4. 他们也应该知道的越少越好。这是对橡胶软管密码分析的保护
    5. 您的凭据应满足所有安全建议(适当的长度、随机性、单一用途等)
    6. 您与远程服务的连接必须是安全的(SSL 等)
    7. 您的远程服务必须是可信的(参见第 1-4 点)。加上它应该容易被黑客入侵(如果您的数据/服务不安全,那么保护您的凭据是没有意义的)。另外它不应该存储您的凭据

    加上我可能忘记的一千件事:)

    【讨论】:

    • 您的回答以一种概括的方式涵盖了客户端和服务器端的保护步骤,但非常清晰且“可遵循”。我很喜欢它! [+1] --- 有几件事我认为应该进一步解释一下,因为也存在一些拼写和格式问题,我冒昧地进行了编辑。 --- 大体结构,以及大部分文本,没有改变。我只是添加了我认为缺少的内容,并重新组织了现有文本以适应它。希望你不要介意。
    • 我不介意拼写、链接、语法等。谢谢。但是,如果你想添加一些东西,请不要改变我的答案。如果您觉得缺少某些内容,请添加评论或创建自己的答案。我更喜欢只用我自己的话签名
    • 我明白了。 ---好吧,我的编辑并没有以任何方式真正改变您答案的含义。其中大部分是修复拼写和格式,而需要修复的格式无论如何都需要轻微的文本更改。少数额外的解释只是对已经说过的话的扩展。 --- 在任何情况下,请修正拼写(短语开头的大写是主要问题)和格式(正确地将“主题”与“内容”分开),对文本进行必要的调整。另外,参见#7 的“prone”。 --- 当然,在做这件事时考虑到额外的因素会很好。
    【解决方案5】:

    如果您无法信任程序运行的环境,但需要通过普通密码或证书进行身份验证,则您无法保护您的凭据。您最多只能使用其他答案中描述的方法来混淆它们。

    作为一种解决方法,我会通过您可以信任的代理运行对 RESTful api 的所有请求,并从那里进行明文密码身份验证。

    【讨论】:

    • “如果您不能信任您的程序运行的环境,...,您将无法保护您的凭据。” - 如果这是真的,几乎每个具有凭据“自动填充”选项的应用程序都会遇到非常严重的麻烦。 ___ 许多两端应用程序(这个问题是什么?)如多人游戏和基于 Web 的应用程序将帐户凭据存储在本地,并且它们很少有任何严重的安全问题。 ___ 无论环境如何,数据都永远不会 100% 安全。受信任的环境只是另一个安全(“更安全”)步骤。
    • 好吧,在给定的场景中,您可以混淆您的凭据,但无法达到 100%(加密)安全性。您最多可以期望的是让攻击者获取明文密码变得如此复杂,以至于不值得他们付出努力。获取典型基于 Web 的应用程序的存储密码所需要做的就是转到浏览器的选项菜单并选择“显示密码”。
    • 您永远无法达到 100% 的安全性,无论是在这种情况下还是在任何其他情况下。这是因为最终,这一切都归结为内存中的0 和1 序列,这是遵循一组特定的逻辑规则实现的,这些逻辑规则本质上总是可逆的。 ___ 加密安全的基础一直是,并且可能永远是,“让它变得如此困难,不值得付出努力。” ___ 最后,你误会了浏览器的自动填充/登录(用于网站),使用应用程序的自动身份验证/登录(保存到加密文件中)。
    • 你应该阅读密码学。有多种方法可以不可逆地加密数据,只需查看“单向哈希”(md5)或公钥加密,即使您同时拥有加密数据和加密密钥,也无法解密加密数据。使用这些方法,您可以获得实际的 100% 安全性。
    • 其实没有。 - 就像我说的,这些方法遵循一组特定的逻辑规则,并且这些规则有反转的方法。 ___ 在加密散列函数的情况下,如果黑客知道生成散列的规则集,他可以重新生成原始数据的几位,并大致了解原始数据的长度可能是多少。 --- 这是很多蛮力和猜测,但它不是 100% 牢不可破的。而且它远非 100% 安全的无限次尝试 ___ 我认为任何黑客都不会费心尝试强硬;无论回报如何,这都不值得付出努力。
    【解决方案6】:

    加密凭据通常不是一个好建议。加密的东西可以被解密。常见的最佳实践是将密码存储为salted hash。哈希无法解密。添加盐以通过Rainbow Tables 击败暴力猜测。只要每个 userId 都有自己的随机盐,攻击者就必须为盐的每个可能值生成一组表,从而在整个宇宙的生命周期内迅速使这种攻击变得不可能。这就是为什么如果您忘记了密码,网站通常无法向您发送密码,而只能“重置”它。他们没有存储您的密码,只有密码的哈希值。

    密码散列不是很难自己实现,但它是一个非常常见的问题,以至于无数其他人为你完成了它。我发现jBcrypt 易于使用。

    作为防止暴力猜测密码的额外保护措施,通常的最佳做法是在使用错误密码进行一定次数的登录尝试后强制用户 ID 或远程 IP 等待几秒钟。没有这个,暴力攻击者每秒可以猜出你的服务器可以处理的尽可能多的密码。每 10 秒猜出 100 个密码或 100 万个密码之间存在巨大差异。

    我觉得您在源代码中包含了用户名/密码组合。这意味着如果您想更改密码,您将不得不重新编译、停止并重新启动您的服务,这也意味着任何掌握您的源代码的人也拥有您的密码。常见的最佳实践是永远不要这样做,而是将凭据(用户名、密码哈希、密码盐)存储在数据存储中

    【讨论】:

    • 我仍然不确定,但我认为 "... 我正在尝试 从 RESTful api ..." 表示OP 不是在谈论服务器端环境。我相信他正在谈论一个通过服务器进行身份验证的客户端应用程序。 __ 因此,客户端应该只保护存储中的凭证(加密)并将它们安全地发送到服务器(TSL / SSL - 它固有地应用加密和消息摘要) ___ 消息摘要(用于注册或比较) 应该只在服务器端完成,否则会不安全。 ___ 这一切都在我的答案中。
    • 另外,您的回答表明使用了可能已过时的 API(jBcrypt - 它在 beta v0.3 上,最后一次更新是在 2010 年 1 月,这可能表明该项目已经消亡) . Java already has it's own standard message-digesting classes,我认为没有必要使用 3rd 方 API。
    • 看起来您对客户端与服务器端的混淆是正确的。我仍然建议将凭据放在数据存储中,而不是在源代码中,但在这种情况下你需要加密而不是散列是对的。
    • Bcrypt 不是消息摘要,而是基于河豚的密钥生成方案。我将它用作 SpringSecurity 的一部分,它非常活跃。普通消息摘要算法(例如 SHA-1 或 MD5)不适用于密码散列,而是用于快速散列。如果您需要尽可能快地散列一段视频或文本,您可以使用这些,或更现代的替代品。如果您对散列密码感兴趣,那么速度就是您的敌人。使用的散列算法越快,蛮力攻击就可以越快成功。
    • 嗯。一些谷歌搜索表明我 Blowfish 是一种加密(可解密),而 jBcrypt 的页面表明它使用基于河豚的消息摘要(a cryptographic hash function)......我很困惑。 ___ SpringSecurity 还活着,Bcrypt 可能还没有;它们是单独的项目。 ___ 无论如何,Java 1.7 已经包含了河豚密码,并且 Security 类的模块化结构允许它作为 security.Provider 的简单实现,即使在旧版本中也是如此,所以我仍然认为不需要 3rd-party API .
    【解决方案7】:

    如果您使用基本身份验证,则应将其与 SSL 结合使用,以避免以 base64 编码的纯文本形式传递您的凭据。您不想让嗅探您的数据包的人轻松获取您的凭据。此外,不要在源代码中硬编码您的凭据。使它们可配置。从配置文件中读取它们。您应该在将凭据存储到配置文件之前对其进行加密,并且您的应用应在从配置文件中读取凭据后对其进行解密。

    【讨论】:

    猜你喜欢
    • 2014-02-04
    • 2016-04-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-06-13
    • 1970-01-01
    • 1970-01-01
    • 2018-09-11
    相关资源
    最近更新 更多