65.9K
CodeProject 正在变化。 阅读更多。
Home

创建和共享(与客户端应用程序)随机加密密钥

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (17投票s)

2010年10月4日

CPOL

6分钟阅读

viewsIcon

88360

downloadIcon

453

一种创建完全随机加密密钥并与客户端应用程序共享的方法

screenshot.jpg

引言

上周,有人提出了一个关于生成随机加密密钥的问题,由于我以前也做过类似的事情,所以我想我会尝试重现我过去的工作,并发表一篇关于它的文章。

左键单击 gadget 并拖动以移动它。左键单击 gadget 的右下角并拖动以调整其大小。右键单击 gadget 以访问其属性。

其思想是创建一个完全随机的密钥,用它加密所需的数据,然后以某种方式与消费者共享该密钥,以便他们能够解密数据,将密钥和数据都传递给消费者。本文提供了创建密钥的代码,而实际的加密和数据传输则留给程序员想象。

创建密钥

我从一个字符串数组开始。更具体地说,是一个转换为字符串的 GUID 数组。为了举例,数组的大小被保持在一个合理的范围内,但是代码的性质使得您可以轻松地扩展数组,使其对于想要逆向工程代码的人来说更“有趣”。唯一的限制是每个数组元素的大小必须完全相同(在本例中,字符串都是 32 个字符长)。

protected string[] m_markers = new string[11] {"E388e564Bb4040E78fB6268120b5521D",
                                               "84525762d2Ef46178BbEe9CA60cFaBa7",
                                               "245Aa8A0cDe44342A9F0f3E598fD41cA",
                                               "dCc0A41302c345F59698d5CaE32a4AcE",
                                               "08C134fE645847108f5A09a4B7310c10",
                                               "071dBf900Ec1413FaA8bC42f153E1648",
                                               "6A189FdAeD894DaB93590a413CdA1fB6",
                                               "28aCe9C59cE04925815cCfBeA9eC4b04",
                                               "E83a43D7d48E40e5B6b20Df8B72b2708",
                                               "25277a837B554306847dAfDc285E455b",
                                               "2A06dF9f35E04F54a783D4bE6cF00d0B",
                                              };

我还设置了一些属性,允许程序员对密钥的创建进行一些控制。

public int        KeyMinLength 
{ 
	get { return m_keyMinLength; }
	set { m_keyMinLength = value; }
}
/// 
/// Get/set the maximum possible key length
/// 
public int        KeyMaxLength 
{ 
	get { return m_keyMaxLength; }
	set { m_keyMaxLength = value; }
}
/// 
/// Get/set the key direction (how the marker array is traversed while 
/// building the key)
/// 
public Directions Direction 
{ 
	get { return m_direction; }
	set { m_direction = value; }
}
/// 
/// Get/set the actual length of the key
/// 
public int        ActualKeyLength 
{ 
	get { return m_keyActualLength; }
	set { m_keyActualLength = value; }
}
/// 
/// Get/set the descriptor length
/// 
public int        StaticDescriptorLength 
{ 
	get { return m_staticDescriptorLength;  }
	set { m_staticDescriptorLength = value; }
}

其中最有趣的是 Direction 属性。它代表一组标志,这些标志指示在构建密钥时遍历标记数组的方向和方向。可以指定以下方向:

  • 左侧
  • 右侧
  • 向上
  • 向下
  • Horizontal
  • 垂直
  • 对角线

当然,同时向左和向右,或向上和向下是没有意义的,所以该类会采取合理的预防措施来防止指定无效方向,它会在实际尝试构建密钥之前调用 ReasonableDirection 方法。它只是执行一个健全性检查,以确保程序员没有做一些奇怪的事情,比如在水平方向上同时指定左右。

//--------------------------------------------------------------------------------
/// 
/// Determines if the direction specified is "reasonable"
/// 
/// 
public bool ReasonableDirection()
{
	bool reasonable   = true;
	int  orientations = 0;
	int  horizontals  = 0;
	int  verticals    = 0;
	orientations += HasDirection(Directions.Horizontal) ? 1 : 0;
	orientations += HasDirection(Directions.Vertical)   ? 1 : 0;
	orientations += HasDirection(Directions.Diagonal)   ? 1 : 0;
	reasonable    = (orientations == 1);
	if (reasonable)
	{
		horizontals += HasDirection(Directions.Left)  ? 1 : 0;
		horizontals += HasDirection(Directions.Right) ? 1 : 0;
		verticals   += HasDirection(Directions.Up)    ? 1 : 0;
		verticals   += HasDirection(Directions.Down)  ? 1 : 0;
		if (HasDirection(Directions.Horizontal))
		{
			reasonable = (horizontals == 1);
		}
		else if (HasDirection(Directions.Vertical))
		{
			reasonable = (verticals == 1);
		}
		else 
		{
			reasonable = (horizontals == 1 && verticals == 1);
		}
	}
	return reasonable;
}

screenshot2.jpg

上图说明了 Direction 属性可能实现的一些可能性。

  • 青色线表示两种可能的方向 - Right|Down|Diagonal(从第 0 行,第 1 列开始),或 Left|Up|Diagonal(从第 7 行,第 8 列开始)。
  • 红色线表示两种可能的方向 - Right|Horizontal(从第 5 行,第 12 列开始),或 Left|Horizontal(从第 5 行,第 20 列开始)。
  • 绿线表示两种可能的方向 - Up|Vertical(从第 3 行,第 24 列开始),或 Down|Vertical(从第 7 行,第 24 列开始)。请注意,这将导致代码换行到最远的边缘。
  • 黄线表示一种可能的方向 - Right|Horizontal(从第 9 行,第 28 列开始)。还要注意,代码不仅会换行,还会移动到下一行。这表明 Direction 属性还包括 RowIncrement 标志。此标志仅适用于 Horizontal 方向,并会导致代码换行到数组中下一最低行(无论指定的水平方向如何)。

请记住,如果您允许类随机选择一个方向,则完全有可能最终得到一个所有字符都相同的密钥。如果 Direction 最终是 Diagonal 且起始位置在数组的一个角落,则会发生这种情况。如果这困扰您,您可以随时更改随机位置选择代码以省略极端角落。就我个人而言,我看不出偶尔发生这种情况有什么问题,因为每个数据项都可以用不同的加密密钥发送。

最后,我们来到 ConstructKey 方法。此方法使用 Direction 属性来确定如何遍历标记数组。这是通过使用调整器来控制的,这些调整器控制如何索引到数组以及数组中的项。首先,我们运行合理性检查。如果存在问题,我们抛出异常:

private void ConstructKey()
{
	if (!ReasonableDirection())
	{
		throw new Exception("The specified Direction value indicates either both left and right, or both up and down.");
	}

接下来,我们设置索引调整器

	// assume horizontal left-to-right
	int  rowAdjuster   = 0;
	int  colAdjuster   = 1;
	bool wrapToNextRow = false;

	// establish our adjusters so we can index to the correct spot in the markers array
	if (HasDirection(Directions.Horizontal))
	{
		colAdjuster   = HasDirection(Directions.Left) ? -1 : 1;
		wrapToNextRow = HasDirection(Directions.RowIncrement);
		// wrapping to next row will always wrap down do the next row, even 
		// if you're going left
	}
	else if (HasDirection(Directions.Vertical))
	{
		rowAdjuster = HasDirection(Directions.Up) ? -1 : 1;
		colAdjuster = 0;
	}
	else // Directions.Diagonal
	{
		rowAdjuster = HasDirection(Directions.Up) ? -1 : 1;
		colAdjuster = HasDirection(Directions.Left) ? -1 : 1;
	}

然后,为了方便输入,我们确定在哪里换行以及换行到哪里。我使用“枢轴”这个词,仅仅因为我喜欢“枢轴”这个词。

	int rowPivotAt = (rowAdjuster == -1) ? 0                       : m_markers.Length - 1;
	int rowPivotTo = (rowAdjuster == -1) ? m_markers.Length - 1    : 0;
	int colPivotAt = (colAdjuster == -1) ? 0                       : m_markers[0].Length - 1;
	int colPivotTo = (colAdjuster == -1) ? m_markers[0].Length - 1 : 0;
	// this is where the magic will start
	int currRow    = ArrayIndex;
	int currCol    = MarkerIndex;

最后,我们开始实际遍历数组。因为我们上面设置了所有控制变量,所以循环清晰、紧凑且易于阅读。

	StringBuilder key = new StringBuilder("");
	for (int keyCount = 0; keyCount < ActualKeyLength; keyCount++)
	{
		// snatch the character at the current row/column
		key.Append(m_markers[currRow][currCol]);
		// adjust our row/column based on the adjusters (and where we are i the array)
		currCol = (currCol == colPivotAt) ? colPivotTo : currCol + colAdjuster;
		currRow = (currRow == rowPivotAt) ? rowPivotTo : currRow + rowAdjuster;
		// if we need to, wrap to the next row
		if (wrapToNextRow && currCol == colPivotTo)
		{
			// remember, we always wrap down to the next row
			currRow++;
			currRow = (currRow == rowPivotAt) ? rowPivotTo : currRow;
		}
	}
	// set our Key property with the results
	Key = key.ToString();
}

描述符

现在,您可能在想,“好吧,太棒了!我们有一个随机化的密钥。接下来呢?”

任何了解传输安全信息的人都知道,您不能只是明文传输这些东西。在我看来,它完全可以通过“描述”您正在发送的内容而不泄露全部信息。这就是“描述符”的作用。

还记得那些用于实际构建我们的随机密钥的巧妙控制变量吗?好吧,您所要做的就是将这些信息提供给一个引用了用于构建密钥的相同程序集的客户端应用程序,然后它就可以根据这些值解码密钥。

所以,假设我们的密钥最终长度为 14 个字符,起始标记数组索引为 5,标记索引(我们在数组元素 5 中索引到标记的位置)为 28,并且我们的 DirectionDiagonal | Up | Left。我们只需调用 GetKeyDescriptor 方法来构建我们的描述符字符串。

public string GetKeyDescriptor()
{
	StringBuilder descriptor = new StringBuilder();
	descriptor.AppendFormat("{0},", ActualKeyLength);
	descriptor.AppendFormat("{0},", ArrayIndex);
	descriptor.AppendFormat("{0},", MarkerIndex);
	descriptor.AppendFormat("{0},", (int)Direction);
	descriptor.Append(MakePadding(descriptor.Length));
	KeyDescriptor = descriptor.ToString();
	return KeyDescriptor;
}

您可能已经注意到了对 Append 的最后一次调用。这有两个目的:a) 作为验证描述符字符串有效负载的一种方式,b) 让黑客们绞尽脑汁试图弄清楚那个大块字节意味着什么。这是该方法:

private string MakePadding(int currLength)
{
	int maxLength = StaticDescriptorLength - currLength;
	if (maxLength <= 0)
	{
		// if you get this exception, you need to specify a higher value for 
		// the StaticDescriptorLength property.
		throw new Exception("Descriptor string is already longer than the value indicated by the static descriptor length.");
	}
	StringBuilder padding = new StringBuilder("");
	while (padding.Length < maxLength)
	{
		padding.Append(Guid.NewGuid().ToString("N"));
	}
	return (padding.ToString().Substring(0, maxLength));
}

此方法的目的是创建足够的随机填充字符,以将字符串填充到指定的长度。如您所见,它只是创建新的 GUID 并将其附加到填充字符串,直到它超过必要的长度,然后调用 Substring 方法来截断所有我们不需要的部分。

最后,接收描述符字符串的客户端应用程序需要一种方法来在它自己的端重新创建密钥。我们通过重载 BuildKey 方法来实现这一点,其中一个版本接受描述符字符串并解析该字符串。

public void BuildKey(string descriptor)
{
	Key             = "";
	KeyDescriptor   = descriptor;
	ActualKeyLength = 0;
	ArrayIndex      = -1;
	MarkerIndex     = -1;
	Direction       = Directions.None;

	if (descriptor.Length == StaticDescriptorLength)
	{
		string[] parts = descriptor.Split(',');
		int temp;
		if (Int32.TryParse(parts[0], out temp))
		{
			ActualKeyLength = temp;
		}
		if (Int32.TryParse(parts[1], out temp))
		{
			ArrayIndex = temp;
		}
		if (Int32.TryParse(parts[2], out temp))
		{
			MarkerIndex = temp;
		}
		if (Int32.TryParse(parts[3], out temp))
		{
			Direction = (Directions)(temp);
		}
		if (ActualKeyLength > 0 && ArrayIndex >= 0 && MarkerIndex >= 0 && Direction != Directions.None)
		{
			ConstructKey();
		}
	}
}

如果您的网站(或 Web 服务)和您的客户端应用程序都使用此程序集(或基于它的程序集),则您可以为每个独立的数据块发送具有不同加密密钥的数据,使其随机性如此之高,以至于破解加密几乎不可能。为了进一步迷惑黑客,您可以大约每 30 天发布一个包含完全不同标记字符串集的新程序集。结合客户端应用程序的 ClickOnce 部署,事情将保持同步,而您无需费力。

示例应用程序

示例应用程序仅允许您指定一些条件,然后生成一个随机密钥(以及描述符字符串)。表单中的最后一个控件显示了从描述符密钥中的信息重建的密钥(只是为了证明它有效)。

最后

此代码只是一个更大的代码套件的一部分,该套件将允许安全数据传输而不会牺牲数据完整性,并且应与应用程序标识符、程序集混淆和签名以及其他技术等其他措施结合使用。使用描述符字符串,您甚至可以指定要使用的加密算法,甚至可以加密描述符字符串以增加更多安全性。

几年前我编写这段代码时,我只是加密了描述符字符串并将其附加到我正在发送的加密数据中。客户端的代码知道该怎么做,因为它有相同的程序集。

历史

  • 2010 年 10 月 4 日 - 原始版本。
© . All rights reserved.