有个项目需要用到SM3做摘要算法,在github上找了一个实现,很多博客里用的也是这一套,先附上链接:SM3-JAVA实现
看文件开头的声明类似下面,就基本是同一套实现方案了
算法的实现是通过获取源文件byte[],先调用一个叫padding的方法对末位填充,然后提取hash,起初因为只需要处理文本、图片等小文件,只加了一个接收inputstream的方法,调用原算法的内容转byte[],直到后来需要处理视频等大文件,生产上报了内存溢出,因为算法的所有分支,最终都是获取完整的byte[],没办法处理大文件
先看下原算法获取完整byte[]后,调用padding填充:
private static byte[] padding(byte[] source) throws IOException {
if (source.length >= 0x2000000000000000l) {
throw new RuntimeException("src data invalid.");
}
long l = source.length * 8;
long k = 448 - (l + 1) % 512;
if (k < 0) {
k = k + 512;
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
baos.write(source);
baos.write(FirstPadding);
long i = k - 7;
while (i > 0) {
baos.write(ZeroPadding);
i -= 8;
}
baos.write(long2bytes(l));
return baos.toByteArray();
}
然后提取hash:
public static byte[] hash(byte[] source) throws IOException {
byte[] m1 = padding(source, null);
int n = m1.length / (512 / 8);
byte[] b;
byte[] vi = IV.toByteArray();
byte[] vi1 = null;
for (int i = 0; i < n; i++) {
b = Arrays.copyOfRange(m1, i * 64, (i + 1) * 64);
vi1 = CF(vi, b);
vi = vi1;
}
return vi1;
}
重点在这行:b = Arrays.copyOfRange(m1, i * 64, (i + 1) * 64);
可以看到原算法获取到完成的byte[]后,也是分64字节来处理的,可以参考这个实现,传入inputstream,每次读取64个字节,最后一次再调用padding做填充
参考hash(byte[])的实现,增加方法hash(InputStream):
private static byte[] hash(InputStream inputStream) throws IOException {
byte[] vi = IV.toByteArray();
byte[] vi1 = null;
byte[] bytes = new byte[64];
int length = -1;
int totalLength = 0;
while ((length = inputStream.read(bytes)) != -1) {
totalLength += length;
if (inputStream.available() == 0) {
bytes = padding(Arrays.copyOfRange(bytes, 0, length), totalLength);
//=========================补充更正=========================
//如果padding后长度 > 64,分两次处理,感谢评论区朋友的提醒
if (bytes.length > 64) {
for (int i = 0; i <= 1; i++) {
byte[] b = Arrays.copyOfRange(bytes, i * 64, (i + 1) * 64);
vi1 = CF(vi, b);
vi = vi1;
}
break;
}
//=========================补充更正=========================
}
vi1 = CF(vi, bytes);
vi = vi1;
}
return vi1;
}
需要注意,原来的padding算法最后写入了完整的字节长度:baos.write(long2bytes(l));
所以这里对padding方法增加一个入参length,记录字节数组长度,原来的调用传null,根据参数是否为空来设置最终写入的值:
private static byte[] padding(byte[] source, Integer length) throws IOException {
if (source.length >= 0x2000000000000000l) {
throw new RuntimeException("src data invalid.");
}
long l = length == null ? source.length * 8 : length * 8;
long k = 448 - (l + 1) % 512;
if (k < 0) {
k = k + 512;
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
baos.write(source);
baos.write(FirstPadding);
long i = k - 7;
while (i > 0) {
baos.write(ZeroPadding);
i -= 8;
}
baos.write(long2bytes(l));
return baos.toByteArray();
}
区别主要是入参和 第四行 l 的长度计算
后来测试发现这个填充没有的话也不影响最终生成的hash,如果最终填充的long小于实际长度的话会影响,应该是做一个校验用的,新增的调整最好不影响原算法的处理逻辑