算法 · 2025年5月29日

Delphi基于国密SM3的加盐算法实现

这个是本人原创,请在引用时保留本人名号;非常感谢CnPack的CnSM3!

该算法可用于密码保护,基于国密SM3,并进行了加盐及混淆处理,比较安全!

可实现的功能:将密码进行加密后可直接存储至数据库;另外可直接调用函数直接进行密码校验;

{******************************************************************************}
{                          基于国密SM3的加盐密钥算法                           }
{                                  Chrysalis                                   }
{                                QQ:342667266                                  }
{                   ------------------------------------                       }
{                                                                              }
{            本单元是完全开源的,您可以遵照Apache License 2.0的发布            }
{        协议来修改和重新发布这一程序。                                        }
{                                                                              }
{            发布这一单元的目的是希望它有用,但没有任何担保。甚至没有          }
{        适合特定目的而隐含的担保。                                            }
{                                                                              }
{            如果您有更好的代码,在修改后还望抄送一份给我,谢谢                }
{            电子邮件:342667266@qq.com                                        }
{                                                                              }
{******************************************************************************}
unit uSM3Salt;
{* |<PRE>
================================================================================
* 单元名称:基于国密SM3的加盐密钥算法实现单元
* 单元作者:Chrysalis
* 备    注:本单元是在调用CnPack 开发组的算法库CnSM3的基础上,
*           实现了国家商用密码 SM3 杂凑算法加盐处理。
*           参考国密算法公开文档《SM3 Cryptographic Hash Algorith》
*           http://www.oscca.gov.cn/UpFile/20101222141857786.pdf
*           CnPack 算法包
*           https://www.cnpack.org/download.php?id=757&lang=zh-cn
*
* 开发平台:Windows 10 + Delphi XE11
* 兼容测试:无
* 本 地 化:该单元中的字符串均符合本地化处理方式
* 修改记录:2025.05.29 V0.1
*               移植并创建单元
*
================================================================================
|</PRE>}


{
使用方法:
1. 将密码加密后存入数据库
   var
     sPassWord : string;
   begin
     sPassWord := '12?3aB#323';
     Result := HashPassword(sPassWord);  //其中Result可直接存储至数据库
    //$gs$432137$79ZjUxOGY4OTMyMzcyNDI4ZjhlMTYzNzBlZDRhYmU0Zjc=$63QTk0NDQ0RUNDQTEwQjZENURCOUEzRDdFNzM2NkI2ODkwRkRFRDIyNjNDMTc4RTJGNUQ4QzMwRkU0NTY5RENEQQ==
   end;

2. 使用校验
   var
     sPassWord, sPassWord_DB : string;     //sPassWord 需要校验的密码明文   sPassWord_DB 数据库中存储的加密字符串
   begin
     sPassWord := '12?3aB#323';
     sPassWord_DB := LoadFromDB;  //从数据库中获取的需要比较的加密字符串
     if CheckPassword(sPassWord, sPassWord_DB) then
       showmessage('PASS')
     else
       showmessage('NoPASS')
   end;

特别说明:
  1. 目前盐 使用GUID值,比较单一,可自行生成更复杂的盐
  2. 目前混淆算法除了常用的前缀法和后缀法以外,还自行编写了交叉法以及交叉倒叙法,加强混淆
  3. 在加密密码时,故意增加了一个小的校验值;目的是干扰破解者视线以及增加一点点的校验功能
}



interface

uses
  system.SysUtils, System.NetEncoding, System.Classes, System.Math, CnSM3;

const
  ConfusionModeCount = 6;
  divisor = 100;

type
  TConfusionMode = (
                    CM_Prefix = 1,     //前缀
                    CM_Suffix = 2,     //后缀
                    CM_Cross_P = 3,    //交叉_由Passphrase开始
                    CM_Cross_S = 4,    //交叉_由salt开始
                    CM_Cross_P_D = 5,  //交叉_由Passphrase开始_倒序
                    CM_Cross_S_D = 6   //交叉_由salt开始_倒序
                   );

  /// <summary>加盐函数</summary>
  /// <remarks> 通过该函数,可以将原始密码进行加盐混淆处理 </remarks>
  /// <param name="Passphrase">String 输入的原始密码</param>
  /// <param name="salt">string 盐字符串</param>
  /// <param name="PP">Integer 混淆算法ID</param>
  /// <returns>string</returns>
  function merge(Passphrase:string; salt:string; PP:Integer):string;


  /// <summary>密码生成</summary>
  /// <remarks> 通过该函数,可生成用于数据库存储的密钥 </remarks>
  /// <param name="Passphrase">String 输入的原始密码</param>
  /// <returns>string</returns>
  function HashPassword(const Passphrase : String) : string;

  /// <summary>密码校验</summary>
  /// <remarks> 通过该函数,可生成用于明文密码与数据库存储的密钥对比 </remarks>
  /// <param name="Passphrase">String 需校验的密码</param>
  /// <param name="ExpectedHashString">String 数据库中存储密钥加密后的字符串</param>
  /// <returns>Boolean</returns>
  function CheckPassword(const Passphrase : String; ExpectedHashString:string):Boolean;

implementation


function merge(Passphrase:string; salt:string; PP:Integer):string;
var
  I : Integer;
  _iMod : Integer;
  _iLen_P, _iLen_S : Integer;
begin
  Result := '';
  if Passphrase = '' then Exit;
  _iMod := PP mod ConfusionModeCount;

  case _iMod of
    Ord(CM_Prefix) :
    begin
      //前缀
      Result := salt + Passphrase;
    end;
    Ord(CM_Suffix) :
    begin
      //后缀
      Result := Passphrase + salt;
    end;
    Ord(CM_Cross_P) :
    begin
      //交叉_由Passphrase开始
      _iLen_P := Length(Passphrase);
      _iLen_S := Length(salt);
      for I := 1 to Max(_iLen_P, _iLen_S) do
      begin
        if I < _iLen_P then
          Result := Result + Passphrase[I];
        if I < _iLen_S then
          Result := Result + salt[I];
      end;
    end;
    Ord(CM_Cross_S) :
    begin
      //交叉_由salt开始
      _iLen_P := Length(Passphrase);
      _iLen_S := Length(salt);
      for I := 1 to Max(_iLen_P, _iLen_S) do
      begin
        if I < _iLen_S then
          Result := Result + salt[I];
        if I < _iLen_P then
          Result := Result + Passphrase[I];
      end;
    end;
    Ord(CM_Cross_P_D) :
    begin
      //交叉_由Passphrase开始_倒序
      _iLen_P := Length(Passphrase);
      _iLen_S := Length(salt);
      for I := Max(_iLen_P, _iLen_S) downto 1 do
      begin
        if I < _iLen_P then
          Result := Result + Passphrase[I];
        if I < _iLen_S then
          Result := Result + salt[I];
      end;
    end;
    Ord(CM_Cross_S_D) :begin
      //交叉_由salt开始_倒序
      _iLen_P := Length(Passphrase);
      _iLen_S := Length(salt);
      for I := Max(_iLen_P, _iLen_S) downto 1 do
      begin
        if I < _iLen_S then
          Result := Result + salt[I];
        if I < _iLen_P then
          Result := Result + Passphrase[I];
      end;
    end;
  end;
end;

function HashPassword(const Passphrase : String) : string;
var
  _BytesSrc, _ByteSalt : TBytes;
  _Passphrase, _sSrc, _sDec : String;
  _iLen, _iPP, _iS , _iP : Integer;
  _sSalt : string;
  function GetGUID: string;
  var
    LTep: TGUID;
    sGUID: string;
  begin
    CreateGUID(LTep);
    sGUID := GUIDToString(LTep);
    sGUID := StringReplace(sGUID, '-', '', [rfReplaceAll]);
    sGUID := Copy(sGUID, 2, Length(sGUID) - 2);
    Result := LowerCase(sGUID);
  end;

begin
{
		Generate a password hash, letting sm3 decide the best parameters.

		Password hash is returned as a string of the form:

			$gs$ppsspp$salt$hash
				pp   - model
        ss   - salt check
        ps   - password check

				salt - base64 encoded salt 1-16 bytes decoded
				hash - base64 encoded 64-byte scrypt hash

		For example.
			Password: "correct horse battery staple"
			Hash: "$gs$124454$G34Rvmk2DSkp9sFJyyM49O$z3XxEUNlHDhq2nCR1Yh4tqKCelFQ9gnFNgtmgoBJHW4zeAIDoAjV5zcOLYk5lLqoGEFQNQ6YoOvXHAlVjPJS9e"
          pp                      12
          ss                      44
          ps                      55
					Salt (base64):          G34Rvmk2DSkp9sFJyyM49O
					Password (base64):      z3XxEUNlHDhq2nCR1Yh4tqKCelFQ9gnFNgtmgoBJHW4zeAIDoAjV5zcOLYk5lLqoGEFQNQ6YoOvXHAlVjPJS9e
}
  try
    Result := '';
    _Passphrase := Trim(Passphrase);
    _iLen := Length(_Passphrase);
    if _iLen <= 0 then Exit;
    //salt   generate salt guid
    _sSalt := GetGUID;
    _iPP := Random(divisor-1);
    //merge
    _sSrc := merge(_Passphrase, _sSalt, _iPP);
//    _ByteSalt := TEncoding.Unicode.GetBytes(_sSalt);
    _BytesSrc := TEncoding.Unicode.GetBytes(_sSrc);
    if Length(_BytesSrc) <= 0 then Exit;
    _iS := Random(divisor-1);
    _iP := Random(divisor-1);
    _sSalt := Format('%.2d',[divisor - _iS]) + StringReplace(TNetEncoding.Base64.Encode(_sSalt), #13#10, '', [rfReplaceAll]);
    _sDec  := Format('%.2d',[divisor - _iP]) + StringReplace(TNetEncoding.Base64.Encode(SM3Print(SM3Bytes(_BytesSrc))), #13#10, '', [rfReplaceAll]);
    Result := Format('$gs$%.2d%.2d%.2d$%s$%s', [_iPP, _iS, _iP, _sSalt, _sDec])
  except
    on E:Exception do
    begin
      raise Exception.Create(E.Message);
    end;
  end;
end;


function CheckPassword(const Passphrase : String; ExpectedHashString:string):Boolean;
var
  _BytesSrc, _ByteSalt : TBytes;
  _Passphrase, _sSrc, _sExpect, _sDec1, _sDec2 : String;
  _iLen, _iPP, _iS , _iP, _iTmp : Integer;
  _sSalt, _sParam, _sTmp : string;
  _sList : TStringList;
begin
{
		Generate a password hash, letting sm3 decide the best parameters.

		Password hash is returned as a string of the form:

			$gs$ppsspp$salt$hash
				pp   - model
        ss   - salt check
        ps   - password check

				salt - base64 encoded salt 1-16 bytes decoded
				hash - base64 encoded 64-byte scrypt hash

		For example.
			Password: "correct horse battery staple"
			Hash: "$gs$124454$G34Rvmk2DSkp9sFJyyM49O$z3XxEUNlHDhq2nCR1Yh4tqKCelFQ9gnFNgtmgoBJHW4zeAIDoAjV5zcOLYk5lLqoGEFQNQ6YoOvXHAlVjPJS9e"
          pp                      12
          ss                      44
          ps                      55
					Salt (base64):          G34Rvmk2DSkp9sFJyyM49O
					Password (base64):      z3XxEUNlHDhq2nCR1Yh4tqKCelFQ9gnFNgtmgoBJHW4zeAIDoAjV5zcOLYk5lLqoGEFQNQ6YoOvXHAlVjPJS9e
	}
  _sList := TStringList.Create;
  try
  try
    Result := False;
    _Passphrase := Trim(Passphrase);
    _iLen := Length(_Passphrase);
    if _iLen <= 0 then Exit;
    _sExpect := Trim(ExpectedHashString);
    if _sExpect = '' then Exit;
    //分解对比函数
    _sList.StrictDelimiter := True ;      //排除空格的方式
    _sList.Delimiter      := '$';
    _sList.DelimitedText  := _sExpect;
    if _sList.Count <> 5 then Exit;
    if _sList.Strings[1] <> 'gs' then Exit;
    _sParam := Trim(_sList.Strings[2]);
    if Length(_sParam) <> 6 then Exit;
    //解析Param参数部分
    _iPP   := StrToIntDef(_sParam[1] + _sParam[2], 0);
    _iS    := StrToIntDef(_sParam[3] + _sParam[4], 0);
    _iP    := StrToIntDef(_sParam[5] + _sParam[6], 0);
    //校验并解析salt
    _sTmp := trim(_sList.Strings[3]);
    if Length(_sTmp) < 2 then Exit;
    _iTmp := StrToIntDef(_sTmp[1] + _sTmp[2], 0);
    if _iTmp + _iS <> divisor then Exit;
    _sTmp := Copy(_sTmp, 3, Length(_sTmp) - 2);
    _sSalt := TNetEncoding.Base64.Decode(_sTmp);
    _sTmp := trim(_sList.Strings[4]);
    if Length(_sTmp) < 2 then Exit;
    _iTmp := StrToIntDef(_sTmp[1] + _sTmp[2], 0);
    if _iTmp + _iP <> divisor then Exit;
    _sTmp := Copy(_sTmp, 3, Length(_sTmp) - 2);
    _sDec1 := TNetEncoding.Base64.Decode(_sTmp);
    //merge
    _sSrc := merge(_Passphrase, _sSalt, _iPP);
//    _ByteSalt := TEncoding.Unicode.GetBytes(_sSalt);
    _BytesSrc := TEncoding.Unicode.GetBytes(_sSrc);
    if Length(_BytesSrc) <= 0 then Exit;
    _sDec2 := SM3Print(SM3Bytes(_BytesSrc));
    if _sDec1 = _sDec2 then Result := True;
  except
    on E:Exception do
    begin
      raise Exception.Create(E.Message);
    end;
  end;
  finally
    if Assigned(_sList) then FreeAndNil(_sList);
  end;
end;

initialization
  Randomize; //初始化随机数发生器


end.
Pascal