遍历 table 并使用 SHA256 base64 哈希对字段进行编码
Loop through a table and encode a field with SHA256 base64 hash
我需要使用自己的 SHA256 base64 密码更新当前包含纯文本密码的列。为此,我使用游标遍历每条记录并对密码进行编码,但执行后所有记录都具有相同的编码密码。
DECLARE @hash AS VARBINARY(128);
DECLARE @h64 AS VARCHAR(128);
DECLARE @pass AS VARCHAR(500);
DECLARE @id AS INTEGER;
DECLARE cursor1 CURSOR
FOR SELECT [ID] FROM dbo.Table
OPEN cursor1
FETCH NEXT FROM cursor1 INTO @id
WHILE @@FETCH_STATUS = 0
BEGIN
SET @pass = (SELECT [Password] FROM dbo.Table WHERE ID = @id);
SET @hash = HASHBYTES('SHA2_256', @pass);
SET @h64 = CAST(N'' AS xml).value('xs:base64Binary(sql:variable("@hash"))', 'varchar(128)');
UPDATE dbo.Table SET [Password] = @h64;
FETCH NEXT FROM cursor1 INTO @id;
END;
DEALLOCATE cursor1;
- Don't hash passwords without using a salt!
- 理想情况下使用专为密码哈希设计的哈希函数,如 bcrypt 而不是 SHA 系列(因为 bcrypt 具有可配置的强度值,并将所有参数和字段包含在单个字符串值中,而手动哈希意味着需要存储散列和盐分开)。
- 但是 SQL 服务器本身不支持 bcrypt。
- 同时避免使用
PWDENCRYPT
,因为它已被弃用并且不允许您指定使用的哈希算法,请改用 HASHBYTES
。
- 您仍然可以使用
CRYPT_GEN_RANDOM
安全地生成盐,这是一种加密安全的 RNG(它使用操作系统提供的值,可能是 PRNG 或基于硬件的 RNG)。
- 请注意,在没有
WHERE
子句的 UPDATE
语句中使用 CRYPT_GEN_RANDOM
是安全的,因为它会为每一行生成一个新数字。
- 您不需要游标 - 您可以在单个
UPDATE
语句中完成此操作。
UPDATE
语句无论是否与游标一起使用,其行为都是相同的 - 因此,如果要更新单行,则必须指定 WHERE [primaryKey] = pkValue
子句。
- 始终避免将二进制数据存储为 Base64 编码的字符串 - 将二进制数据存储为
binary(n)
或 varbinary(n)
。
- 这是因为SQL默认使用不区分大小写的排序规则,但是Base64是区分大小写的(Base16是不区分大小写的),所以在Base64列上进行查询可能会return不正确的结果.
- Base64 值比
binary
值多 space 33% - 并且 encoding/decoding 使每个操作的成本更高。
- Base64 值在已经使用
binary
值的查询中使用时不可 SARGable,除非您对所有值进行 Base64 编码 - 这很愚蠢。
这是我的做法(假设我不能使用 bcrypt):
ALTER TABLE
dbo.Table
ADD
[Salt] binary(16) NULL;
GO -- `GO` is necessary when using ALTER TABLE statements in the same query-batch as UPDATE statements.
ALTER TABLE
dbo.Table
ADD
[PasswordHash] binary(32) NULL; -- store hashes as binary values, not strings. SHA-256 produces a 256-bit (32-byte) long hash, so use a fixed-length `binary(32)` column.
GO
-- The [Salt] and [PasswordHash] columns need to be set in separate `UPDATE` queries (or using a single `UPDATE FROM` query) because of how `CRYPT_GEN_RANDOM` works.
UPDATE
dbo.Table
SET
[Salt] = CRYPT_GEN_RANDOM( 16 );
UPDATE
dbo.Table
SET
[PasswordHash] = HASHBYTES( 'SHA_256', [Password] + [Salt] );
-- T-SQL uses `+` to concatenate binary values. Don't use `CONCAT` because it will return a `varchar` value with undefined conversion semantics from `binary` values.
GO
-- Finally, remove the old password information and make the new columns non-NULLable:
ALTER TABLE dbo.Table DROP COLUMN [Password];
GO
ALTER TABLE dbo.Table ALTER COLUMN [Salt] binary(16) NOT NULL;
GO
ALTER TABLE dbo.Table ALTER COLUMN [PasswordHash] binary(32) NOT NULL;
GO
关于 RAND
与 CRYPT_GEN_RANDOM
的注释
如前所述,CRYPT_GEN_RANDOM
是加密安全的 RNG,而 RAND
不是,因此 RAND
不得用于生成加密盐值。
但我想证明另一个有趣的区别:RAND()
将 return 查询中每一行的相同值,而 CRYPT_GEN_RANDOM
总是 return 不同值。通过 运行 这个查询亲自查看:
DECLARE @foo TABLE (
rowId int NOT NULL IDENTITY PRIMARY KEY,
cgr binary(16) NULL,
rng binary(16) NULL,
rng2 binary(16) NULL
);
INSERT INTO @foo ( cgr, rng, rng2 ) VALUES
( NULL, NULL, NULL ),
( NULL, NULL, NULL ),
( NULL, NULL, NULL ),
( NULL, NULL, NULL ),
( NULL, NULL, NULL );
SELECT * FROM @foo;
UPDATE
@foo
SET
cgr = CRYPT_GEN_RANDOM( /*length:*/ 10 ),
rng = RAND();
--
SELECT * FROM @foo;
--
DECLARE @i int = 1;
WHILE @i <= 5
BEGIN
UPDATE
@foo
SET
rng2 = RAND()
WHERE
rowId = @i;
SET @i = @i + 1;
END;
SELECT * FROM @foo;
给我这个最终输出:
id cgr rng rng2
1 0x2DEB1D8A8DAB1F65373E000000000000 0x00000000000000003FC75AD042AE086F 0x00000000000000003FE2C5C607959DFF
2 0x4F7F050C335330AF43E6000000000000 0x00000000000000003FC75AD042AE086F 0x00000000000000003FEB46BAA0391C3E
3 0xB23F1C1C4C860A9652EE000000000000 0x00000000000000003FC75AD042AE086F 0x00000000000000003FDA62960990C897
4 0x44C604D79B0BB19167F9000000000000 0x00000000000000003FC75AD042AE086F 0x00000000000000003FC04FEA23759748
5 0xCF7F9A4FA4EDD605ECC2000000000000 0x00000000000000003FC75AD042AE086F 0x00000000000000003FE3A8FA18BD83A9
请注意 cgr
值是如何唯一的,而 rng
值是完全相同的 - 尽管两列都在同一个 UPDATE
语句中设置。 rng2
列具有不同的值,但这只是因为每一行都是在 WHILE
循环中单独设置的。
(rng
和 rng2
列都以 0x00...003F...
开头,因为 RAND()
return 是 float
(IEEE-754) 值它具有定义的二进制表示形式)。
我需要使用自己的 SHA256 base64 密码更新当前包含纯文本密码的列。为此,我使用游标遍历每条记录并对密码进行编码,但执行后所有记录都具有相同的编码密码。
DECLARE @hash AS VARBINARY(128);
DECLARE @h64 AS VARCHAR(128);
DECLARE @pass AS VARCHAR(500);
DECLARE @id AS INTEGER;
DECLARE cursor1 CURSOR
FOR SELECT [ID] FROM dbo.Table
OPEN cursor1
FETCH NEXT FROM cursor1 INTO @id
WHILE @@FETCH_STATUS = 0
BEGIN
SET @pass = (SELECT [Password] FROM dbo.Table WHERE ID = @id);
SET @hash = HASHBYTES('SHA2_256', @pass);
SET @h64 = CAST(N'' AS xml).value('xs:base64Binary(sql:variable("@hash"))', 'varchar(128)');
UPDATE dbo.Table SET [Password] = @h64;
FETCH NEXT FROM cursor1 INTO @id;
END;
DEALLOCATE cursor1;
- Don't hash passwords without using a salt!
- 理想情况下使用专为密码哈希设计的哈希函数,如 bcrypt 而不是 SHA 系列(因为 bcrypt 具有可配置的强度值,并将所有参数和字段包含在单个字符串值中,而手动哈希意味着需要存储散列和盐分开)。
- 但是 SQL 服务器本身不支持 bcrypt。
- 同时避免使用
PWDENCRYPT
,因为它已被弃用并且不允许您指定使用的哈希算法,请改用HASHBYTES
。
- 您仍然可以使用
CRYPT_GEN_RANDOM
安全地生成盐,这是一种加密安全的 RNG(它使用操作系统提供的值,可能是 PRNG 或基于硬件的 RNG)。- 请注意,在没有
WHERE
子句的UPDATE
语句中使用CRYPT_GEN_RANDOM
是安全的,因为它会为每一行生成一个新数字。
- 请注意,在没有
- 理想情况下使用专为密码哈希设计的哈希函数,如 bcrypt 而不是 SHA 系列(因为 bcrypt 具有可配置的强度值,并将所有参数和字段包含在单个字符串值中,而手动哈希意味着需要存储散列和盐分开)。
- 您不需要游标 - 您可以在单个
UPDATE
语句中完成此操作。UPDATE
语句无论是否与游标一起使用,其行为都是相同的 - 因此,如果要更新单行,则必须指定WHERE [primaryKey] = pkValue
子句。
- 始终避免将二进制数据存储为 Base64 编码的字符串 - 将二进制数据存储为
binary(n)
或varbinary(n)
。- 这是因为SQL默认使用不区分大小写的排序规则,但是Base64是区分大小写的(Base16是不区分大小写的),所以在Base64列上进行查询可能会return不正确的结果.
- Base64 值比
binary
值多 space 33% - 并且 encoding/decoding 使每个操作的成本更高。 - Base64 值在已经使用
binary
值的查询中使用时不可 SARGable,除非您对所有值进行 Base64 编码 - 这很愚蠢。
这是我的做法(假设我不能使用 bcrypt):
ALTER TABLE
dbo.Table
ADD
[Salt] binary(16) NULL;
GO -- `GO` is necessary when using ALTER TABLE statements in the same query-batch as UPDATE statements.
ALTER TABLE
dbo.Table
ADD
[PasswordHash] binary(32) NULL; -- store hashes as binary values, not strings. SHA-256 produces a 256-bit (32-byte) long hash, so use a fixed-length `binary(32)` column.
GO
-- The [Salt] and [PasswordHash] columns need to be set in separate `UPDATE` queries (or using a single `UPDATE FROM` query) because of how `CRYPT_GEN_RANDOM` works.
UPDATE
dbo.Table
SET
[Salt] = CRYPT_GEN_RANDOM( 16 );
UPDATE
dbo.Table
SET
[PasswordHash] = HASHBYTES( 'SHA_256', [Password] + [Salt] );
-- T-SQL uses `+` to concatenate binary values. Don't use `CONCAT` because it will return a `varchar` value with undefined conversion semantics from `binary` values.
GO
-- Finally, remove the old password information and make the new columns non-NULLable:
ALTER TABLE dbo.Table DROP COLUMN [Password];
GO
ALTER TABLE dbo.Table ALTER COLUMN [Salt] binary(16) NOT NULL;
GO
ALTER TABLE dbo.Table ALTER COLUMN [PasswordHash] binary(32) NOT NULL;
GO
关于 RAND
与 CRYPT_GEN_RANDOM
的注释
如前所述,CRYPT_GEN_RANDOM
是加密安全的 RNG,而 RAND
不是,因此 RAND
不得用于生成加密盐值。
但我想证明另一个有趣的区别:RAND()
将 return 查询中每一行的相同值,而 CRYPT_GEN_RANDOM
总是 return 不同值。通过 运行 这个查询亲自查看:
DECLARE @foo TABLE (
rowId int NOT NULL IDENTITY PRIMARY KEY,
cgr binary(16) NULL,
rng binary(16) NULL,
rng2 binary(16) NULL
);
INSERT INTO @foo ( cgr, rng, rng2 ) VALUES
( NULL, NULL, NULL ),
( NULL, NULL, NULL ),
( NULL, NULL, NULL ),
( NULL, NULL, NULL ),
( NULL, NULL, NULL );
SELECT * FROM @foo;
UPDATE
@foo
SET
cgr = CRYPT_GEN_RANDOM( /*length:*/ 10 ),
rng = RAND();
--
SELECT * FROM @foo;
--
DECLARE @i int = 1;
WHILE @i <= 5
BEGIN
UPDATE
@foo
SET
rng2 = RAND()
WHERE
rowId = @i;
SET @i = @i + 1;
END;
SELECT * FROM @foo;
给我这个最终输出:
id cgr rng rng2
1 0x2DEB1D8A8DAB1F65373E000000000000 0x00000000000000003FC75AD042AE086F 0x00000000000000003FE2C5C607959DFF
2 0x4F7F050C335330AF43E6000000000000 0x00000000000000003FC75AD042AE086F 0x00000000000000003FEB46BAA0391C3E
3 0xB23F1C1C4C860A9652EE000000000000 0x00000000000000003FC75AD042AE086F 0x00000000000000003FDA62960990C897
4 0x44C604D79B0BB19167F9000000000000 0x00000000000000003FC75AD042AE086F 0x00000000000000003FC04FEA23759748
5 0xCF7F9A4FA4EDD605ECC2000000000000 0x00000000000000003FC75AD042AE086F 0x00000000000000003FE3A8FA18BD83A9
请注意 cgr
值是如何唯一的,而 rng
值是完全相同的 - 尽管两列都在同一个 UPDATE
语句中设置。 rng2
列具有不同的值,但这只是因为每一行都是在 WHILE
循环中单独设置的。
(rng
和 rng2
列都以 0x00...003F...
开头,因为 RAND()
return 是 float
(IEEE-754) 值它具有定义的二进制表示形式)。