【问题标题】:Decrypt in C# a password encrypted in Java在 C# 中解密用 Java 加密的密码
【发布时间】:2016-02-02 05:16:57
【问题描述】:

Openbravo 软件及其衍生软件(例如 unicentaopos)具有以下加密实现,以将数据库密码存储在普通配置文件中。

package com.openbravo.pos.util;

import java.io.UnsupportedEncodingException;
import java.security.*;
import javax.crypto.*;

/**
 *
 * @author JG uniCenta
 */
public class AltEncrypter {

    private Cipher cipherDecrypt;
    private Cipher cipherEncrypt;

    /** Creates a new instance of Encrypter
     * @param passPhrase */
    public AltEncrypter(String passPhrase) {

        try {
            SecureRandom sr = SecureRandom.getInstance("SHA1PRNG");
            sr.setSeed(passPhrase.getBytes("UTF8"));
            KeyGenerator kGen = KeyGenerator.getInstance("DESEDE");
            kGen.init(168, sr);
            Key key = kGen.generateKey();

            cipherEncrypt = Cipher.getInstance("DESEDE/ECB/PKCS5Padding");
            cipherEncrypt.init(Cipher.ENCRYPT_MODE, key);

            cipherDecrypt = Cipher.getInstance("DESEDE/ECB/PKCS5Padding");
            cipherDecrypt.init(Cipher.DECRYPT_MODE, key);
        } catch (UnsupportedEncodingException | NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException e) {
        }
    }

    /**
     *
     * @param str
     * @return
     */
    public String encrypt(String str) {
        try {
            return StringUtils.byte2hex(cipherEncrypt.doFinal(str.getBytes("UTF8")));
        } catch (UnsupportedEncodingException | BadPaddingException | IllegalBlockSizeException e) {
        }
        return null;
    }

    /**
     *
     * @param str
     * @return
     */
    public String decrypt(String str) {
        try {
            return new String(cipherDecrypt.doFinal(StringUtils.hex2byte(str)), "UTF8");
        } catch (UnsupportedEncodingException | BadPaddingException | IllegalBlockSizeException e) {
        }
        return null;
    }    
}

为了加密,使用如下(只加密密码):

 config.setProperty("db.user", jtxtDbUser.getText());
        AltEncrypter cypher = new AltEncrypter("cypherkey" + jtxtDbUser.getText());       
        config.setProperty("db.password", "crypt:" + cypher.encrypt(new String(jtxtDbPassword.getPassword())));

要解密,使用以下内容:

  String sDBUser = m_App.getProperties().getProperty("db.user");
  String sDBPassword = m_App.getProperties().getProperty("db.password");
  if (sDBUser != null && sDBPassword != null && sDBPassword.startsWith("crypt:")) {
     AltEncrypter cypher = new AltEncrypter("cypherkey" + sDBUser);
     sDBPassword = cypher.decrypt(sDBPassword.substring(6));
   }

我正在使用 C# 开发一个独立的软件模块,我想从该配置文件中读取数据库密码。关于如何实现这一点的任何建议?

通过分析代码,我可以推断:

  1. 密码“加密”是可逆的,因为它后来在软件中用于构建数据库连接字符串。
  2. 基本密码是“cypherkey”+用户名
  3. 密码存储在普通文件中,格式为

db.password=crypt:XXX

其中 XXX 是加密密码。

请帮我弄清楚如何解密密码。不需要帮助实际读取纯文件。请假设我已经在 C# 程序的变量中存储了用户名和加密密码(没有“crypt:”部分)。

我一直在尝试修改有关类似问题的现有示例,但它们专注于 AES,到目前为止我还没有成功。

基本上,应该在 C# 中构建以下函数:

private string DecryptPassword(string username, string encryptedPassword)

我该怎么做?

该软件是开源的,可以找到here

一个测试用例:DecryptPassword("mark", "19215E9576DE6A96D5F03FE1D3073DCC") 应该返回密码getmeback。基本密码是cypherkeymark。我在不同的机器上测试过,使用相同的用户名,“散列”密码总是相同的。

【问题讨论】:

  • SHA1 是单向哈希;不是可逆的密码系统。
  • 这件事比这更复杂,它使用一个伪随机数生成器,使用 SHA1 以密码的 UTF-8 编码为种子,生成一个 168 位密钥,然后在 Encrypt 中增加三倍 DES -Decrypt-Encrypt ("EDE") 模式用于str 参数的加密(这确实是可逆的)。如果 C# 中不存在 SecureRandom.getInstance("SHA1PRNG"); 的等价物,那你就完蛋了。一旦你有了它,你所需要的就是再次派生原始密钥并通过三重DES解密运行encryptedPassword。如果不介意可以发布一些测试数据进行测试吗?
  • 确实,这似乎不是 SHA1,而是自定义的加密/解密方法。标题已编辑。我知道密码是可逆的,因为它稍后在原始软件中用于构建数据库连接字符串。该软件是开源的,可以在here找到一个测试用例:DecryptPassword("mark", "19215E9576DE6A96D5F03FE1D3073DCC")应该返回密码getmeback。基本密码是“cypherkeymark”。我在不同的机器上测试过,相同用户名的“散列”密码总是相同的。
  • 只是对您的评论 @MaximilianGerhardt 的一个小修正:“它使用使用 SHA1 的伪随机数生成器,该生成器使用 passphrase 的 UTF-8 编码来生成168 位密钥”。

标签: java c# encryption cryptography


【解决方案1】:

AltEncrypter 从密码中获取密钥的方法很糟糕。应该使用这种方法。

首先,它不安全。密钥派生算法是不安全的,除非它是计算密集型的。相反,请使用 scrypt、bcrypt 或 PBKDF2 等算法。

其次,SHA1PRNG 算法定义不明确。说“它使用 SHA-1”是不够的。多久执行一次哈希?它不是标准化的;您将无法在另一个平台(如 .Net)上请求“SHA1PRNG”,并获得相同的输出。

所以,放弃这种加密方法并使用由知识渊博的人编写和维护的简单安全的东西。

不幸的是,问题并没有就此结束。 AltEncrypter 实用程序以最糟糕的方式使用,使用非机密密钥对身份验证密码进行可逆加密。这根本不安全。它允许攻击者解密用户密码并将其用于其他系统上的用户帐户。

这几乎就像这个系统的作者想要制造一场安全灾难。

【讨论】:

  • 我吓坏了,我查了一下这个包裹。显然,“pos”包是用于“销售点”的。我不是这么想的。
  • 引用SHA1PRNG is a proprietary algorithm supplied by Sun.(来自www-01.ibm.com/support/docview.wss?uid=swg21205388)完成了这在C#中很难做到,除非有人对Java代码进行逆向工程。确实,在此设置中,这更像是使用已知参数而不是加密的密码编码,您很好地解释了巨大的安全问题,+1。编辑:或者不是,经过一番谷歌搜索后,我找到了源代码参考 (opensource.apple.com/source/gcc3/gcc3-1161/libjava/gnu/java/…),如果有人将此移植到 C#,一切都很好。
  • 感谢您的洞察力,@erickson。我在问题中添加了更多信息(软件源和测试用例)。我想采纳你关于抓取这种加密方法的建议,但遗憾的是我不负责那个项目。我只是想做一个扩展库存功能的 C# 模块,我需要读取密码来构建数据库连接字符串。我没有用 Java 做,因为那不是我的大本营。
【解决方案2】:

这是一个注释。我无法添加 cmets,但我认为用于加密的算法不是 SHA1。它是“DESEDE/ECB/PKCS5Padding”查看创建要加密的密码的行(获得) cipherEncrypt = Cipher.getInstance("DESEDE/ECB/PKCS5Padding");

SHA1PRNG 是一个伪随机数生成器,用于生成用于加密过程的第一个随机数,以便即使在相同的纯文本被加密时也能生成“不同”的加密。

另一个重要的事情是用于加密的密钥,我的意思是:

 KeyGenerator kGen = KeyGenerator.getInstance("DESEDE");
 kGen.init(168, sr);
 Key key = kGen.generateKey(); <-- this key

此密钥用于加密和解密,但我看不到它的存储位置。我的意思是它每次都会重新生成。它应该从某个地方存储和检索,而不是重新生成,因为如果不使用相同的密钥,就不可能解密任何密文。

【讨论】:

  • 伪数生成器始终使用用户名启动,因此对于一个用户,您将始终获得相同的密钥,该密钥进一步用于加密/解密密码。
  • 正如@MJar 所说。如果您查看类签名public AltEncrypter(String passPhrase),加密时发送的密码只是cypherkey + 用户名。所以如果用户名是mark,那么发送的passPhrase参数就是cypherkeymark
【解决方案3】:

这是使用一些解决方法的答案。

我尝试重新实现 SHA1PRNG 提供的 GNU 实现(即 opensource),但它并没有给出与适当的 SUN 实现相同的结果(所以要么它们不同,要么我已经实现了它以错误的方式)。所以我实现了一个解决方法:调用一个 java 程序来为我们派生密钥。是的,这很便宜,但暂时可以解决。如果有人在我的 SHA1PRNG 实现中发现错误,请告诉我。

首先,这是一个简单的 Java 程序,它将使用 SHA1PRNG 生成器在给定种子的情况下派生一个 168 位密钥。只需将其输出到stdout,空格分隔。

import java.io.UnsupportedEncodingException;
import java.security.*;
import javax.crypto.*;

public class PasswordDeriver {

    public static void main(String[] args) throws NoSuchAlgorithmException, UnsupportedEncodingException {
        if(args.length == 0){
            System.out.println("You need to give the seed as the first argument.");
            return;
        }
        //Use Java to generate the key used for encryption and decryption.
        String passPhrase = args[args.length-1];

        SecureRandom sr = SecureRandom.getInstance("SHA1PRNG");
        sr.setSeed(passPhrase.getBytes("UTF8"));
        KeyGenerator kGen = KeyGenerator.getInstance("DESEDE");
        kGen.init(168, sr);
        Key key = kGen.generateKey();

        //Key is generated, now output it. 
        //System.out.println("Format: " + key.getFormat());
        byte[] k = key.getEncoded();
        for(int i=0; i < k.length; i++){
            System.out.print(String.format((i == k.length - 1) ? "%X" : "%X ", k[i]));
        }        
    }
}

这保存为PasswordDeriver.java,使用javac &lt;file&gt; 编译,然后将生成的PasswordDeriver.class 放在与此编译程序相同的文件夹中:(实际的C# 程序)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Security.Cryptography;
using System.IO;
using System.Diagnostics;

namespace OpenbravoDecrypter
{
    class Program
    {
        static void Main(string[] args)
        {
            var decrypted = Decrypt("19215E9576DE6A96D5F03FE1D3073DCC", "mark");
            Console.ReadLine();
        }

        static string Decrypt(string ciphertext, string username)
        {
            //Ciphertext is given as a hex string, convert it back to bytes
            if(ciphertext.Length % 2 == 1) ciphertext = "0" + ciphertext; //pad a zero left is necessary
            byte[] ciphertext_bytes = new byte[ciphertext.Length / 2];
            for(int i=0; i < ciphertext.Length; i+=2)
                ciphertext_bytes[i / 2] = Convert.ToByte(ciphertext.Substring(i, 2), 16);

            //Get an instance of a tripple-des descryption
            TripleDESCryptoServiceProvider tdes = new TripleDESCryptoServiceProvider();        
            tdes.Mode = CipherMode.ECB; //ECB as Cipher Mode
            tdes.Padding = PaddingMode.PKCS7; //PKCS7 padding (same as PKCS5, good enough)

            byte[] key_bytes = DeriveKeyWorkAround(username);
            Console.WriteLine("Derived Key: " + BitConverter.ToString(key_bytes));
            //Start the decryption, give it the key, and null for the IV.
            var decryptor = tdes.CreateDecryptor(key_bytes, null);
            //Decrypt it.
            var plain = decryptor.TransformFinalBlock(ciphertext_bytes, 0, ciphertext_bytes.Length);

            //Output the result as hex string and as UTF8 encoded string
            Console.WriteLine("Plaintext Bytes: " + BitConverter.ToString(plain));
            var s = Encoding.UTF8.GetString(plain);
            Console.WriteLine("Plaintext UTF-8: " + s);
            return s;
        }

        /* Work around the fact that we don't have a C# implementation of SHA1PRNG by calling into a custom-prepared java file..*/
        static byte[] DeriveKeyWorkAround(string username)
        {
            username = "cypherkey" + username;
            string procOutput = "";
            //Invoke java on our file
            Process p = new Process();
            p.StartInfo.FileName = "cmd.exe";
            p.StartInfo.Arguments = "/c java PasswordDeriver \"" + username + "\"";
            p.StartInfo.RedirectStandardOutput = true;
            p.OutputDataReceived += (e, d) => procOutput += d.Data;
            p.StartInfo.UseShellExecute = false;
            p.Start();
            p.BeginOutputReadLine();
            p.WaitForExit();

            //Convert it back
            byte[] key = procOutput.Split(' ').Select(hex => Convert.ToByte(hex, 16)).ToArray();

            return key;
        }


        /* This function copies the functionality of the GNU Implementation of SHA1PRNG. 
         * Currently, it's broken, meaning that it doesn't produce the same output as the SUN implenetation of SHA1PRNG.
         * Case 1: the GNU implementation is the same as the SUN implementation, and this re-implementation is just wrong somewhere
         * Case 2: the GNU implementation is not the same the SUN implementation, therefore you'd need to reverse engineer some existing
         * SUN implementation and correct this method. 
       */
        static byte[] DeriveKey(string username)
        {
            //adjust
            username = "cypherkey" + username;
            byte[] user = Encoding.UTF8.GetBytes(username);
            //Do SHA1 magic
            var sha1 = new SHA1CryptoServiceProvider();
            var seed = new byte[20];
            byte[] data = new byte[40];
            int seedpos = 0;
            int datapos = 0;

            //init stuff
            byte[] digestdata;
            digestdata = sha1.ComputeHash(data);
            Array.Copy(digestdata, 0, data, 0, 20);

            /* seeding part */
            for (int i=0; i < user.Length; i++)
            {
                seed[seedpos++ % 20] ^= user[i];
            }
            seedpos %= 20;

            /* Generate output bytes */
            byte[] bytes = new byte[24]; //we need 24 bytes (= 192 bit / 8)

            int loc = 0;
            while (loc < bytes.Length)
            {
                int copy = Math.Min(bytes.Length - loc, 20 - datapos);

                if (copy > 0)
                {
                    Array.Copy(data, datapos, bytes, loc, copy);
                    datapos += copy;
                    loc += copy;
                }
                else
                {
                    // No data ready for copying, so refill our buffer.
                    Array.Copy(seed, 0, data, 20, 20);
                    byte[] digestdata2 = sha1.ComputeHash(data);
                    Array.Copy(digestdata2, 0, data, 0, 20);
                    datapos = 0;
                }
            }
            Console.WriteLine("GENERATED KEY:\n");
            for(int i=0; i < bytes.Length; i++)
            {
                Console.Write(bytes[i].ToString("X").PadLeft(2, '0'));
            }

            return bytes;
        } 
    }
}

您可以看到标准内容,例如初始化三重 DES 密码提供程序,为其提供密钥并计算其中的密文解密。它还包含当前损坏的 SHA1PRNG 实现和解决方法。鉴于java 在当前环境变量的PATH 中,这个程序产生输出:

派生密钥:86-EF-C1-F2-2F-97-D3-F1-34-49-23-89-E3-EC-29-80-02-92-52-40-49-5D- CD-C1

明文字节:67-65-74-6D-65-62-61-63-6B

纯文本 UTF-8:getmeback

所以,这里有解密功能(加密也是一样,只需将.CreateDecryptor() 更改为.CreateEncryptor())。如果您忘记了执行密钥派生的代码,那么解密代码只需要大约 20 行代码即可完成工作。因此,回顾一下,我的回答是其他想要将此解决方案 100% C# 的人的起点。希望这会有所帮助。

【讨论】:

  • 如果 OP 采用这种方法,那么性能更好的替代方案(与创建新进程相比)可能是让程序监听 socket 并接受密码作为请求和返回编码的密钥作为响应。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-09-29
  • 1970-01-01
  • 1970-01-01
  • 2013-04-19
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多