精华 个人的一些 Node.js Crypto 与 OpenSSL 密码学笔记分享
发布于 2 个月前 作者 EasonWang01 1573 次浏览 最后一次编辑是 1 个月前 来自 分享

以下是自己先前在研究密码学时使用 Node.js Crypto 模块和 OpenSSL 为范例做的笔记,和大家分享一下。

GitHub 文章连结

目錄


相關密碼學介紹

密碼學被廣泛應用在現今的應用中,例如晶片、信用卡、電子郵件加密傳輸、身分認證、遠端電腦連線等等。

在現代許多協定均有使用到密碼學來進行加解密的動作,例如 : HTTPS、PGP、IPsec、SSL / TLS、SSH。

以下介紹將雜湊函式( Hash )、對稱式加密、非對稱式加密,並且會以 OpenSSL 與 Node.js 進行範例實作。


OpenSSL

https://www.openssl.org/

可使用以下指令來查看OpenSSL版本

openssl version -a

OpenSSL為一個開放原始碼的函式庫套件,用C語言寫成,類Unix系統已經內建在裡面,而Windows使用者可以用Git Bash來執行。

OpenSSL計劃在1998年開始,目標是發明一套自由的加密工具,其包含目前大部分的主流加密演算法。

查看所有可用指令與可用加密法

openssl help

而我們也可以輸入以下指令,測試電腦執行相關加密演算法時的性能與耗時

openssl speed

其他指令可參考:

https://wiki.openssl.org/index.php/Command_Line_Utilities

Node.js Crypto模組

https://nodejs.org/api/crypto.html

可使用以下指令來查看Node.js crypto模組

node -p crypto

取得可用Hash function

node -p crypto.getHashes()

取得可用之對稱式加密方法

node -p crypto.getCiphers()

有關雜湊(Hash)

Hash function也可稱為雜湊函式,可以把一串任意長度大小的字串或數字,轉換一串為固定大小的值。

而任何相同的值經過hash後出來的值都會是相同的,所以就可以進一步做到訊息驗證。

通常是一個單向函式,也就是無法從算出來的值反推回原本的值 ( 除了已經被破解的Hash算法例外 )。

比較常見的包含MD5、SHA1、SHA2、SHA3與Ripemd系列等等,但目前MD5與SHA1已經被破解。

而還有一些是專門用來幫密碼做Hash的雜湊函式,例如:

bcryptPBKDF2scryptArgon2等等。

雜湊函式被設計成能夠非常快速,讓整個加密程序不至於被拖慢,但這樣攻擊者可以非常快速的用任意的字串來進行雜湊並比對輸出結果,所以在雜湊中會加入一些方法來解決此問題。

1.Salt

Salt是指在雜湊函數中或是密碼上加入一串隨機的位元 ( random sequence of bytes )。
所以密碼就會像是:salt字串 + password字串
而任何人知道 password 字串並進行雜湊後的雜湊值,將不會與加入salt後的值相同。
如果每個使用者都有不同的salt,就沒有簡單的方法來辨識出使用者是否使用相同的password字串。

2.Stretching

意思就是讓我們雜湊過的數字再次進行雜湊,所以要得到相同的Hash的話對方必須也要進行相同次數的雜湊才可以。

3.Memory

在計算時將大量數字暫存在記憶體中,如此一來電腦在運算時也必須消耗足夠的記憶體,
使用大量的記憶體也代表了這個雜湊函式不容易進行平行處理(parallelize)。

我們下面一樣使用Node.js來展示一些Hash function讓大家可以快速地了解他們的用法。

MD5

const crypto = require('crypto');

function md5(text) {
  return crypto.createHash('md5').update(text).digest('hex');
};

let hash = md5('test');
console.log(hash);

SHA

全名為 Secure Hash Algorithm,從1995年發佈的SHA1、2001年發佈的SHA2、2015年正式發佈的SHA3,每一代都是上一代的改進版本,而SHA1的Hash值長度為160bits,到了SHA2與SHA3他們的長度可以是以下幾種,例如:224、256、384、512等等,所以我們才會常聽到例如SHA-256或是SHA-512等等的名詞。

SHA-256範例:

const crypto = require('crypto');
let hash = crypto.createHash('sha256').update('test').digest('hex'); //hex代表輸出為16進位,原先為buffer型態
console.log(hash);

SHA3

而在2015年8月5日SHA-3正式發表,而由於原先的Keccak被選為SHA3的最適合算法,所以SHA3也稱為Keccak

以下為Node.js SHA3範例:

npm install sha3@1.2.0
const SHA3 = require('sha3');

let hash_512 = new SHA3.SHA3Hash().update('test').digest('hex'); //預設為512bits
console.log(hash_512);

let hash_224 = new SHA3.SHA3Hash(224).update('test').digest('hex'); // 224 bits
console.log(hash_224);

Ripemd

Ripemd系列包含128、160、256、320等等,數字代表其 hash 過後的 bits 長度,最常見的是Ripemd-160,也就是產生160 bits長度的Hash,而Ripemd主要為了加強與改良原先的MD系列而發明。

Ripemd-160範例:

需要先安裝 ripemd160 第三方模組

npm install ripemd160@2.0.1
const RIPEMD160 = require('ripemd160')

console.log(new RIPEMD160().update('test').digest('hex'))

HMAC

HMAC是在上述的雜湊函式外再加上一個secret,然後一起進行Hash。

const crypto = require('crypto');
const hmac = crypto.createHmac('sha256', 'a secret');

hmac.update('test');
console.log(hmac.digest('hex'));

Blake2

https://blake2.net/

這是一個強調快速、安全與簡單的Hash算法,並且目前被用來替代MD5和SHA1。

因為它可以在不耗費更多資源的情況下提供更好的安全性,但後來由Keccak獲選為SHA3的原因是NIST認為Keccak更具有抗ASIC的能力以及因為Blake的實作類似於SHA2,所以後來才不選擇Blake為SHA3標準。

參考此:https://crypto.stackexchange.com/questions/31674/what-advantages-does-keccak-sha-3-have-over-blake2

Blake2在2012年發表,是Blake的改良版本,列於RFC-7693 https://tools.ietf.org/html/rfc7693

其中分為兩種類型,BLAKE2b是針對64bits的平台進行優化而BLAKE2s是針對8到32bits的平台。

接著我們使用https://github.com/dcposch/blakejs模組

npm install blakejs@1.1.0
const blake = require('blakejs')

// blake2b
console.log(blake.blake2bHex('test'))

// blake2s
console.log(blake.blake2sHex('test'))

Bcrypt、PBKDF2、Scrypt、Argon2

有一些工具函式是專門設計用來將金鑰、密碼、密文做雜湊的函式,這些函式的特點都是運算速度不快,並且有些需要耗費較多的運算記憶體,讓破解者無法在短時間內快速算出對應的雜湊,增加了密碼被破解的難度。

Bcrypt

基於Blowfish的一個加密算法,於1999年發表。

hash的過程會加入一個隨機的salt,然後salt跟password一起hash。但每次產生的salt會不一樣,所以同一個密碼每次進行產生的Hash會不同。

而Bcrypt包含Round數,也就是要重複進行幾次運算,越多Round會需要越多的計算時間。

以下為在2GHz core上的耗費時間表:

再來你可能會想既然每次產生的Hash不同,那要怎麼進行訊息驗證呢?

解答是:在要驗證訊息時,會從原先的Hash中取出salt ( 通常為Hash前面幾個字 ),然後把取出來的salt跟輸入的password進行hash,最後將得到的結果跟之前保存在資料庫中的Hash值進行比對。

我們可用Node.js的Bcrypt模組:https://github.com/kelektiv/node.bcrypt.js

1.安裝

npm install bcrypt

2.生成Hash

const bcrypt = require('bcrypt');
const saltRounds = 10;
const myPlaintextPassword = 'I_am_password';

bcrypt.genSalt(saltRounds, function (err, salt) {
  bcrypt.hash(myPlaintextPassword, salt, function (err, hash) {
    if (err) console.log(err);
    console.log(hash);
  });
});

3.驗證

const bcrypt = require('bcrypt');
const myPlaintextPassword = 'I_am_password';

bcrypt.compare(myPlaintextPassword, "$2a$10$8QT.28zoo.jyFB2yvDURL.IM6gL4YJHGsr1PUysnFuGeqeDeooxuK", function(err, res) {
   console.log(res)
});

// 在bcrypt.compare()中的填入要比對的密碼與剛才產生出的Hash

PBKDF2

全名為Password-Based Key Derivation Function,利用HMAC的方式來加入password和salt然後一樣進行多次的重複計算。

Node.js原生即提供了PBKDF2算法。

const crypto = require('crypto');
crypto.pbkdf2('secret', 'salt', 100000, 64, 'sha512', (err, derivedKey) => {
  if (err) throw err;
  console.log(derivedKey.toString('hex'));  // '3745e48...08d59ae'
});

其中參數依序為:

password 要進行Hash的字串

salt 加入的隨機值

iterations 計算次數

key_length 產生的Hash長度(bytes)

digest 使用的Hash算法 e.g. SHA-512

Scrypt

此Hash方法加入了需要大量記憶體運算的設計,作法為利用大量記憶體,並將運算資料儲存在記憶體內供演算法計算,如此可避免一些客製化的硬體快速的計算出Hash。而其也是Litecoin與Dogecoin所使用的Hash演算法。

其強調他對抗暴力破解程度是PBKDF2的兩萬倍,是Bcrypt的四千倍。

https://www.tarsnap.com/scrypt.html

其通常包含三個參數N、r、p

N: General work factor, iteration count.(重複計算的次數)
r: blocksize in use for underlying hash; fine-tunes the relative memory-cost.(區塊大小)
p: parallelization factor; fine-tunes the relative cpu-cost.(平行計算的數量)

參考至https://stackoverflow.com/questions/11126315/what-are-optimal-scrypt-work-factors
// 以下為計算Scrypt會需要使用的記憶體大小
128 bytes × N × r
128 × 16384 × 8 = 16,777,216 bytes = 16 MB

而p的參數一般來說都會是1

接著,我們使用node-scrypt模組來進行實際操作。

https://github.com/barrysteyn/node-scrypt

安裝:

npm install scrypt
const scrypt = require("scrypt");

scrypt.kdf("password", { N: 1, r: 1, p: 1 }, function (err, result) {
  scrypt.verifyKdf(result, new Buffer("password"), function (err, result) {
    if (err) console.log(err);
    console.log(result)
  });
});

Argon2

在2015獲選為Password Hashing Competition的冠軍,其有三種類型Argon2i、Argon2d與Argon2id。

其詳細spec可參考: https://github.com/P-H-C/phc-winner-argon2/blob/master/argon2-specs.pdf

Argon2d: (快速,並且可對抗GPU暴力破解攻擊)
Faster and uses data-depending memory access, which makes it highly resistant against GPU cracking attacks 
and suitable for applications with no threats from side-channel timing attacks. 

Argon2i: (適合用於密碼雜湊與金鑰衍伸函式)
Which is preferred for password hashing and password-based key derivation, 
but it is slower as it makes more passes over the memory to protect from tradeoff attacks.

Argon2id: (為前兩者的結合)
Hybrid of Argon2i and Argon2d, using a combination of data-depending and data-independent memory accesses, 
which gives some of Argon2i's resistance to side-channel cache timing attacks and much of Argon2d's resistance to GPU cracking attacks.

參考至: https://github.com/P-H-C/phc-winner-argon2

以下使用Node.js的第三方Argon2模組

npm install argon2

產生Hash

const argon2 = require('argon2');
const options = {
  timeCost: 4, memoryCost: 13, parallelism: 2, type: argon2.argon2d
};

argon2.hash('password', options).then(hash => {
  console.log(hash)
});

驗證

const argon2 = require('argon2');

/*
agron2.verify()的第一個參數填入剛才產生的雜湊值,第二個參數填入雜湊時輸入的密碼
*/
argon2.verify('$argon2d$v=19$m=8192,t=4,p=2$Qt1HCzlwg260X7LNpzqtCg$u+zNJnC2s7gs6vJ6rzlR6usRIKJdvqGGKjALr47txg0', 'password').then(match => {
    if (match) {
      console.log('match!')
    } else {
      console.log('not match.')
    }
  }).catch(err => {
    console.log(err)
});

SHA-256實作原理

第一步:

1.把明文轉為二進位的數字bits
2.在最後面先放一個數字1
3.在最後面補 k 個 0 直到明文bits的長度 + 1 + k % 512 = 448

做完上述三點後,在最後面再加上 64 bits的數字,內容為明文的長度(以二進位表示) ,例如:abc之ASCII二進位長度為24 bits

而 24 之二進位為11000,因為不足 64 bit 於是在前面補0使其成為:00....11000,補足64 bits。

以下為 8-bit 之 ASCII 字母:abc 之例子。對照下圖的第一個 8 bits: 01100001 對應到 a,可參照ASCII Table。

第二步:

寫出八個 initial hash value(此為被預先定義之固定值)

原理:
先取Math.sqrt(n)的小數,然後乘上2 ** 32,之後轉為16進位,然後取小數前面部分
其中math.sqrt(n),n為從0往上找到的最小八個質數。 

H(0)
1 = 6a09e667            
(Math.sqrt(2) % 1 * (2**32)).toString(16).substring(0,8)

H(0)
2 = bb67ae85
(Math.sqrt(3) % 1 * (2**32)).toString(16).substring(0,8)

H(0)
3 = 3c6ef372
(Math.sqrt(5) % 1 * (2**32)).toString(16).substring(0,8)

H(0)
4 = a54ff53a
(Math.sqrt(7) % 1 * (2**32)).toString(16).substring(0,8)

H(0)
5 = 510e527f
(Math.sqrt(11) % 1 * (2**32)).toString(16).substring(0,8)

H(0)
6 = 9b05688c
(Math.sqrt(13) % 1 * (2**32)).toString(16).substring(0,8)

H(0)
7 = 1f83d9ab
(Math.sqrt(17) % 1 * (2**32)).toString(16).substring(0,8)

H(0)
8 = 5be0cd19
(Math.sqrt(19) % 1 * (2**32)).toString(16).substring(0,8)

第三步

寫出 64 個固定 k 值

**原理: **

用Math cube root的方法找首64個質數

(Math.cbrt(2) * (2**32)).toString(16).substring(1,9)
"428a2f98"

(Math.cbrt(3) * (2**32)).toString(16).substring(1,9)
"71374491"

(Math.cbrt(5) * (2**32)).toString(16).substring(1,9)
"b5c0fbcf"

第四步:

使用SHA-256定義的六個邏輯 Function

其中符號定義如下

寫成程式為:

function ch (x, y, z) {
  return z ^ (x & (y ^ z))
}

function maj (x, y, z) {
  return (x & y) | (z & (x | y))
}

function sigma0 (x) {
  return (x >>> 2 | x << 30) ^ (x >>> 13 | x << 19) ^ (x >>> 22 | x << 10)
}

function sigma1 (x) {
  return (x >>> 6 | x << 26) ^ (x >>> 11 | x << 21) ^ (x >>> 25 | x << 7)
}

function gamma0 (x) {
  return (x >>> 7 | x << 25) ^ (x >>> 18 | x << 14) ^ (x >>> 3)
}

function gamma1 (x) {
  return (x >>> 17 | x << 15) ^ (x >>> 19 | x << 13) ^ (x >>> 10)
}

完整範例

需要先安裝big-integer模組

npm install big-integer
const util = require('util');
var bigInt = require("big-integer");


function Hash (blockSize, finalSize) {
  this._block = new Buffer(blockSize)
  this._finalSize = finalSize
  this._blockSize = blockSize
  this._len = 0
}

Hash.prototype.digest = function (data, enc) {

data = (data).toString(); 
//////////////////
let s = ""
let s1 = ""

// 將字串轉為二進位ASCII
function ascii (a) { 
  let dd = "";
  for(let i = 0 ; i< a.length; i++) {
    let charAscII = a.charAt(i).charCodeAt(0).toString(2)
    while(charAscII.length < 8) { // 最前面的0都會被省略,所以手動加上
      charAscII = 0 + charAscII
    }
    dd += charAscII; 
  }
  return dd
}

let msgLengthToBinary = ((ascii(data).length).toString(2).length); // 計算訊息有幾個字元再轉為二進位數字的長度
let BinarymsgLength = (ascii(data).length); // 訊息的二進位的長度

for(i = 0; i < 448 - 1 - BinarymsgLength; i++) {s += 0} // 前面補448 bit 的 0
for(i = 0; i < 64 - msgLengthToBinary ; i++) {s1 += 0} // 後面補64 bit 的 0

let msgLength = ascii(data).length;
let frontPad = ascii(data) + 1 + s + s1 + (ascii(data).length).toString(2);


let c0 = bigInt(bin2dec(frontPad)).toString(16);  //涉及大數轉進位(MAX_SAFE_INTEGER)


let initPadedValue = c0.substring(0, c0.length - 2) + msgLength.toString(16); //因為太大的數parseInt並轉二進位後面的數字會被省略掉,所以把它加上


let initPadedValueBuffer = Buffer.from(initPadedValue, 'hex');

  this._update(initPadedValueBuffer) //傳入一開始pad好之buffer進行main loop hash function
  var hash = this._hash() //拼接main loop好的a-h八個區塊

  return enc ? hash.toString(enc) : hash  //轉為16進位
}


function bin2dec(str){ //大數二進位在用parseInt轉十進位時後面常被省略,所以用此方法
    var dec = str.toString().split(''), sum = [], hex = [], i, s
    while(dec.length){
        s = 1 * dec.shift()
        for(i = 0; s || i < sum.length; i++){
            s += (sum[i] || 0) * 2
            sum[i] = s % 10
            s = (s - sum[i]) / 10
        }
    }
    while(sum.length){
        hex.push(sum.pop().toString(10))
    }
    return hex.join('')
}


var K = [
  0x428A2F98, 0x71374491, 0xB5C0FBCF, 0xE9B5DBA5,
  0x3956C25B, 0x59F111F1, 0x923F82A4, 0xAB1C5ED5,
  0xD807AA98, 0x12835B01, 0x243185BE, 0x550C7DC3,
  0x72BE5D74, 0x80DEB1FE, 0x9BDC06A7, 0xC19BF174,
  0xE49B69C1, 0xEFBE4786, 0x0FC19DC6, 0x240CA1CC,
  0x2DE92C6F, 0x4A7484AA, 0x5CB0A9DC, 0x76F988DA,
  0x983E5152, 0xA831C66D, 0xB00327C8, 0xBF597FC7,
  0xC6E00BF3, 0xD5A79147, 0x06CA6351, 0x14292967,
  0x27B70A85, 0x2E1B2138, 0x4D2C6DFC, 0x53380D13,
  0x650A7354, 0x766A0ABB, 0x81C2C92E, 0x92722C85,
  0xA2BFE8A1, 0xA81A664B, 0xC24B8B70, 0xC76C51A3,
  0xD192E819, 0xD6990624, 0xF40E3585, 0x106AA070,
  0x19A4C116, 0x1E376C08, 0x2748774C, 0x34B0BCB5,
  0x391C0CB3, 0x4ED8AA4A, 0x5B9CCA4F, 0x682E6FF3,
  0x748F82EE, 0x78A5636F, 0x84C87814, 0x8CC70208,
  0x90BEFFFA, 0xA4506CEB, 0xBEF9A3F7, 0xC67178F2
]

var W = new Array(64)

function Sha256 () {
  this.init()

  this._w = W // new Array(64)

  Hash.call(this, 64, 56)
}

util.inherits(Sha256, Hash)

Sha256.prototype.init = function () {
  this._a = 0x6a09e667
  this._b = 0xbb67ae85
  this._c = 0x3c6ef372
  this._d = 0xa54ff53a
  this._e = 0x510e527f
  this._f = 0x9b05688c
  this._g = 0x1f83d9ab
  this._h = 0x5be0cd19

  return this
}

function ch (x, y, z) {
  return z ^ (x & (y ^ z))
}

function maj (x, y, z) {
  return (x & y) | (z & (x | y))
}

function sigma0 (x) {
  return (x >>> 2 | x << 30) ^ (x >>> 13 | x << 19) ^ (x >>> 22 | x << 10)
}

function sigma1 (x) {
  return (x >>> 6 | x << 26) ^ (x >>> 11 | x << 21) ^ (x >>> 25 | x << 7)
}

function gamma0 (x) {
  return (x >>> 7 | x << 25) ^ (x >>> 18 | x << 14) ^ (x >>> 3)
}

function gamma1 (x) {
  return (x >>> 17 | x << 15) ^ (x >>> 19 | x << 13) ^ (x >>> 10)
}

Sha256.prototype._update = function (M) {
  var W = this._w

  var a = this._a | 0
  var b = this._b | 0
  var c = this._c | 0
  var d = this._d | 0
  var e = this._e | 0
  var f = this._f | 0
  var g = this._g | 0
  var h = this._h | 0

  for (var i = 0; i < 16; ++i) W[i] = M.readInt32BE(i * 4)
  for (; i < 64; ++i) W[i] = (gamma1(W[i - 2]) + W[i - 7] + gamma0(W[i - 15]) + W[i - 16]) | 0

  for (var j = 0; j < 64; ++j) {
    var T1 = (h + sigma1(e) + ch(e, f, g) + K[j] + W[j]) | 0
    var T2 = (sigma0(a) + maj(a, b, c)) | 0

    h = g
    g = f
    f = e
    e = (d + T1) | 0
    d = c
    c = b
    b = a
    a = (T1 + T2) | 0
  }

  this._a = (a + this._a) | 0
  this._b = (b + this._b) | 0
  this._c = (c + this._c) | 0
  this._d = (d + this._d) | 0
  this._e = (e + this._e) | 0
  this._f = (f + this._f) | 0
  this._g = (g + this._g) | 0
  this._h = (h + this._h) | 0
}

Sha256.prototype._hash = function () {
  var H = new Buffer(32)

  H.writeInt32BE(this._a, 0)
  H.writeInt32BE(this._b, 4)
  H.writeInt32BE(this._c, 8)
  H.writeInt32BE(this._d, 12)
  H.writeInt32BE(this._e, 16)
  H.writeInt32BE(this._f, 20)
  H.writeInt32BE(this._g, 24)
  H.writeInt32BE(this._h, 28)

  return H
}

// 執行:

let hashString = new Sha256().digest('test','hex');

console.log(hashString)

使用 Node.js 的 crypto 模組進行驗證,可以得到相同的雜湊值。

const crypto = require('crypto');
let result = crypto.createHash('sha256').update('test').digest('hex')
console.log(result)

參考資料:

http://www.iwar.org.uk/comsec/resources/cipher/sha256-384-512.pdf

RIPEMD-160實作原理

步驟ㄧ:

官方公式定義:

根據 j 的值不同,函式計算方式也不同

符號意思分別為

步驟二:

然後加上hash時會用到的常數constant

1.

將其分為hl與hr
const hl = [0x00000000, 0x5a827999, 0x6ed9eba1, 0x8f1bbcdc, 0xa953fd4e];
const hr = [0x50a28be6, 0x5c4dd124, 0x6d703ef3, 0x7a6d76e9, 0x00000000];

對照上圖最右側公式 “5a827999” = (2 ** 30 * (Math.sqrt(2))).toString(16).substring(0,8)

2.selection of message word

寫為如下:

const zl = [
  0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
  7, 4, 13, 1, 10, 6, 15, 3, 12, 0, 9, 5, 2, 14, 11, 8,
  3, 10, 14, 4, 9, 15, 8, 1, 2, 7, 0, 6, 13, 11, 5, 12,
  1, 9, 11, 10, 0, 8, 12, 4, 13, 3, 7, 15, 14, 5, 6, 2,
  4, 0, 5, 9, 7, 12, 2, 10, 14, 1, 3, 8, 11, 6, 15, 13
]

const zr = [
  5, 14, 7, 0, 9, 2, 11, 4, 13, 6, 15, 8, 1, 10, 3, 12,
  6, 11, 3, 7, 0, 13, 5, 10, 14, 15, 8, 12, 4, 9, 1, 2,
  15, 5, 1, 3, 7, 14, 6, 9, 11, 8, 12, 2, 10, 0, 4, 13,
  8, 6, 4, 1, 3, 11, 15, 0, 5, 12, 2, 13, 9, 7, 10, 14,
  12, 15, 10, 4, 1, 5, 8, 7, 6, 2, 13, 14, 0, 3, 9, 11
]

3.Amount for rotate left (rol)

const sl = [
  11, 14, 15, 12, 5, 8, 7, 9, 11, 13, 14, 15, 6, 7, 9, 8,
  7, 6, 8, 13, 11, 9, 7, 15, 7, 12, 15, 9, 11, 7, 13, 12,
  11, 13, 6, 7, 14, 9, 13, 15, 14, 8, 13, 6, 5, 12, 7, 5,
  11, 12, 14, 15, 14, 15, 9, 8, 9, 14, 5, 6, 8, 6, 5, 12,
  9, 15, 5, 11, 6, 8, 13, 12, 5, 12, 13, 14, 11, 8, 5, 6
]

var sr = [
  8, 9, 9, 11, 13, 15, 15, 5, 7, 7, 8, 11, 14, 14, 12, 6,
  9, 13, 15, 7, 12, 8, 9, 11, 7, 7, 12, 7, 6, 15, 13, 11,
  9, 7, 15, 11, 8, 6, 6, 14, 12, 13, 5, 14, 13, 13, 7, 5,
  15, 5, 8, 11, 14, 14, 6, 14, 6, 9, 12, 9, 12, 5, 15, 8,
  8, 5, 12, 9, 12, 5, 14, 6, 8, 13, 6, 5, 15, 13, 11, 11
]
最後寫上initial value

看完上面定義的這些數字可能會有點模糊,不知道他們是用來做什麼的

所以我們直接來看整個hash過程的pseudo-code

寫成JS

function rotl (x, n) {
  return (x << n) | (x >>> (32 - n))
}

function fn1 (a, b, c, d, e, m, k, s) {
  return (rotl((a + (b ^ c ^ d) + m + k) | 0, s) + e) | 0
}

function fn2 (a, b, c, d, e, m, k, s) {
  return (rotl((a + ((b & c) | ((~b) & d)) + m + k) | 0, s) + e) | 0
}

function fn3 (a, b, c, d, e, m, k, s) {
  return (rotl((a + ((b | (~c)) ^ d) + m + k) | 0, s) + e) | 0
}

function fn4 (a, b, c, d, e, m, k, s) {
  return (rotl((a + ((b & d) | (c & (~d))) + m + k) | 0, s) + e) | 0
}

function fn5 (a, b, c, d, e, m, k, s) {
  return (rotl((a + (b ^ (c | (~d))) + m + k) | 0, s) + e) | 0
}

for (var i = 0; i < 80; i += 1) {
    var tl
    var tr
    if (i < 16) {
      tl = fn1(al, bl, cl, dl, el, words[zl[i]], hl[0], sl[i])
      tr = fn5(ar, br, cr, dr, er, words[zr[i]], hr[0], sr[i])
    } else if (i < 32) {
      tl = fn2(al, bl, cl, dl, el, words[zl[i]], hl[1], sl[i])
      tr = fn4(ar, br, cr, dr, er, words[zr[i]], hr[1], sr[i])
    } else if (i < 48) {
      tl = fn3(al, bl, cl, dl, el, words[zl[i]], hl[2], sl[i])
      tr = fn3(ar, br, cr, dr, er, words[zr[i]], hr[2], sr[i])
    } else if (i < 64) {
      tl = fn4(al, bl, cl, dl, el, words[zl[i]], hl[3], sl[i])
      tr = fn2(ar, br, cr, dr, er, words[zr[i]], hr[3], sr[i])
    } else { // if (i<80) {
      tl = fn5(al, bl, cl, dl, el, words[zl[i]], hl[4], sl[i])
      tr = fn1(ar, br, cr, dr, er, words[zr[i]], hr[4], sr[i])
    }

    al = el
    el = dl
    dl = rotl(cl, 10)
    cl = bl
    bl = tl

    ar = er
    er = dr
    dr = rotl(cr, 10)
    cr = br
    br = tr
}

最後,把一開始的constant的每個數值加上新hash過的數字更新然後串接起來

  let t = (this._b + cl + dr) | 0
  this._b = (this._c + dl + er) | 0
  this._c = (this._d + el + ar) | 0
  this._d = (this._e + al + br) | 0
  this._e = (this._a + bl + cr) | 0
  this._a = t

  let buffer = new Buffer(20)
  buffer.writeInt32LE(this._a, 0)
  buffer.writeInt32LE(this._b, 4)
  buffer.writeInt32LE(this._c, 8)
  buffer.writeInt32LE(this._d, 12)
  buffer.writeInt32LE(this._e, 16)
  return buffer

--------------------------------------------------------------------------------------------------------------------

完整程式碼

const util = require('util');

function HashBase(blockSize) {
  this._block = Buffer.allocUnsafe(blockSize)
  this._blockSize = blockSize
  this._blockOffset = 0
  this._length = [0, 0, 0, 0]

  this._finalized = false
}


HashBase.prototype.update = function (data, encoding) {
  if (this._finalized) throw new Error('Digest already called')
  if (!Buffer.isBuffer(data)) data = Buffer.from(data, encoding)

  // consume data
  var block = this._block
  var offset = 0
  while (this._blockOffset + data.length - offset >= this._blockSize) {
    for (var i = this._blockOffset; i < this._blockSize;) block[i++] = data[offset++]
    this._update()
    this._blockOffset = 0
  }
  while (offset < data.length) block[this._blockOffset++] = data[offset++]

  // update length
  for (var j = 0, carry = data.length * 8; carry > 0; ++j) {
    this._length[j] += carry
    carry = (this._length[j] / 0x0100000000) | 0
    if (carry > 0) this._length[j] -= 0x0100000000 * carry
  }

  return this
}

HashBase.prototype._update = function () {
  throw new Error('_update is not implemented')
}

HashBase.prototype.digest = function (encoding) {
  if (this._finalized) throw new Error('Digest already called')
  this._finalized = true

  var digest = this._digest()
  if (encoding !== undefined) digest = digest.toString(encoding)

  // reset state
  this._block.fill(0)
  this._blockOffset = 0
  for (var i = 0; i < 4; ++i) this._length[i] = 0

  return digest
}


var zl = [
  0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
  7, 4, 13, 1, 10, 6, 15, 3, 12, 0, 9, 5, 2, 14, 11, 8,
  3, 10, 14, 4, 9, 15, 8, 1, 2, 7, 0, 6, 13, 11, 5, 12,
  1, 9, 11, 10, 0, 8, 12, 4, 13, 3, 7, 15, 14, 5, 6, 2,
  4, 0, 5, 9, 7, 12, 2, 10, 14, 1, 3, 8, 11, 6, 15, 13
]

var zr = [
  5, 14, 7, 0, 9, 2, 11, 4, 13, 6, 15, 8, 1, 10, 3, 12,
  6, 11, 3, 7, 0, 13, 5, 10, 14, 15, 8, 12, 4, 9, 1, 2,
  15, 5, 1, 3, 7, 14, 6, 9, 11, 8, 12, 2, 10, 0, 4, 13,
  8, 6, 4, 1, 3, 11, 15, 0, 5, 12, 2, 13, 9, 7, 10, 14,
  12, 15, 10, 4, 1, 5, 8, 7, 6, 2, 13, 14, 0, 3, 9, 11
]

var sl = [
  11, 14, 15, 12, 5, 8, 7, 9, 11, 13, 14, 15, 6, 7, 9, 8,
  7, 6, 8, 13, 11, 9, 7, 15, 7, 12, 15, 9, 11, 7, 13, 12,
  11, 13, 6, 7, 14, 9, 13, 15, 14, 8, 13, 6, 5, 12, 7, 5,
  11, 12, 14, 15, 14, 15, 9, 8, 9, 14, 5, 6, 8, 6, 5, 12,
  9, 15, 5, 11, 6, 8, 13, 12, 5, 12, 13, 14, 11, 8, 5, 6
]

var sr = [
  8, 9, 9, 11, 13, 15, 15, 5, 7, 7, 8, 11, 14, 14, 12, 6,
  9, 13, 15, 7, 12, 8, 9, 11, 7, 7, 12, 7, 6, 15, 13, 11,
  9, 7, 15, 11, 8, 6, 6, 14, 12, 13, 5, 14, 13, 13, 7, 5,
  15, 5, 8, 11, 14, 14, 6, 14, 6, 9, 12, 9, 12, 5, 15, 8,
  8, 5, 12, 9, 12, 5, 14, 6, 8, 13, 6, 5, 15, 13, 11, 11
]

var hl = [0x00000000, 0x5a827999, 0x6ed9eba1, 0x8f1bbcdc, 0xa953fd4e]
var hr = [0x50a28be6, 0x5c4dd124, 0x6d703ef3, 0x7a6d76e9, 0x00000000]

function RIPEMD160() {
  HashBase.call(this, 64)

  // state
  this._a = 0x67452301
  this._b = 0xefcdab89
  this._c = 0x98badcfe
  this._d = 0x10325476
  this._e = 0xc3d2e1f0
}

util.inherits(RIPEMD160, HashBase)

RIPEMD160.prototype._update = function () {
  const words = new Array(16)
  for (var j = 0; j < 16; ++j) words[j] = this._block.readInt32LE(j * 4)

  var al = this._a | 0
  var bl = this._b | 0
  var cl = this._c | 0
  var dl = this._d | 0
  var el = this._e | 0

  var ar = this._a | 0
  var br = this._b | 0
  var cr = this._c | 0
  var dr = this._d | 0
  var er = this._e | 0

  // computation
  for (var i = 0; i < 80; i += 1) {
    var tl
    var tr
    if (i < 16) {
      tl = fn1(al, bl, cl, dl, el, words[zl[i]], hl[0], sl[i])
      tr = fn5(ar, br, cr, dr, er, words[zr[i]], hr[0], sr[i])
    } else if (i < 32) {
      tl = fn2(al, bl, cl, dl, el, words[zl[i]], hl[1], sl[i])
      tr = fn4(ar, br, cr, dr, er, words[zr[i]], hr[1], sr[i])
    } else if (i < 48) {
      tl = fn3(al, bl, cl, dl, el, words[zl[i]], hl[2], sl[i])
      tr = fn3(ar, br, cr, dr, er, words[zr[i]], hr[2], sr[i])
    } else if (i < 64) {
      tl = fn4(al, bl, cl, dl, el, words[zl[i]], hl[3], sl[i])
      tr = fn2(ar, br, cr, dr, er, words[zr[i]], hr[3], sr[i])
    } else { // i < 80
      tl = fn5(al, bl, cl, dl, el, words[zl[i]], hl[4], sl[i])
      tr = fn1(ar, br, cr, dr, er, words[zr[i]], hr[4], sr[i])
    }

    al = el
    el = dl
    dl = rotl(cl, 10)
    cl = bl
    bl = tl

    ar = er
    er = dr
    dr = rotl(cr, 10)
    cr = br
    br = tr
  }

  // update state
  var t = (this._b + cl + dr) | 0
  this._b = (this._c + dl + er) | 0
  this._c = (this._d + el + ar) | 0
  this._d = (this._e + al + br) | 0
  this._e = (this._a + bl + cr) | 0
  this._a = t
}

RIPEMD160.prototype._digest = function () {
  // create padding and handle blocks
  this._block[this._blockOffset++] = 0x80
  if (this._blockOffset > 56) {
    this._block.fill(0, this._blockOffset, 64)
    this._update()
    this._blockOffset = 0
  }

  this._block.fill(0, this._blockOffset, 56)
  this._block.writeUInt32LE(this._length[0], 56)
  this._block.writeUInt32LE(this._length[1], 60)
  this._update()

  // produce result
  var buffer = new Buffer(20)
  buffer.writeInt32LE(this._a, 0)
  buffer.writeInt32LE(this._b, 4)
  buffer.writeInt32LE(this._c, 8)
  buffer.writeInt32LE(this._d, 12)
  buffer.writeInt32LE(this._e, 16)
  return buffer
}

function rotl(x, n) {
  return (x << n) | (x >>> (32 - n))
}

function fn1(a, b, c, d, e, m, k, s) {
  return (rotl((a + (b ^ c ^ d) + m + k) | 0, s) + e) | 0
}

function fn2(a, b, c, d, e, m, k, s) {
  return (rotl((a + ((b & c) | ((~b) & d)) + m + k) | 0, s) + e) | 0
}

function fn3(a, b, c, d, e, m, k, s) {
  return (rotl((a + ((b | (~c)) ^ d) + m + k) | 0, s) + e) | 0
}

function fn4(a, b, c, d, e, m, k, s) {
  return (rotl((a + ((b & d) | (c & (~d))) + m + k) | 0, s) + e) | 0
}

function fn5(a, b, c, d, e, m, k, s) {
  return (rotl((a + (b ^ (c | (~d))) + m + k) | 0, s) + e) | 0
}

console.log(new RIPEMD160().update('abc').digest('hex'))

參考至https://www.esat.kuleuven.be/cosic/publications/article-317.pdf

對稱式加密 ( Symmetric-Key Algorithm )

對稱式加密的特性為加密和解密時使用相同的密鑰,常見的對稱式加密算法有:AES、3DES、IDEA、TwoFish、RC系列等等

而DES與BlowFish目前因安全性已不建議採用。

而下面展示一個對稱式加密最基本的原理,假設我們要用以下密鑰 11110011 將密文 Test 加密:

密文: Test
密鑰: 11110011

我們先把密文轉換為ACSII 二進位

function textToBinary(string) {
  return string.split('').map(function (char) {
    return '0' + char.charCodeAt(0).toString(2);
  })
}

textToBinary("Test");
// ["01010100", "01100101", "01110011", "01110100"]

上面每個陣列對應一個英文字母,我們把每個陣列元素與密鑰分別做 XOR 運算(註1):

function XOR(a, b) {
 if(a.length !== b.length) {
   console.log("要求兩者二進位數字長度相同");
   return
 }
 let num = (parseInt(a, 2) ^ parseInt(b, 2)).toString(2);
 if(num.length !== a.length) {  // 因前面為0會被省略,所以要手動補0
    let padLength = a.length - num.length;
    num = "0".repeat(padLength) + num;
  };
  return num 
}

let key_ = "11110011"; // 密鑰

// 加密
(["01010100", "01100101", "01110011", "01110100"]).map(d => XOR(d, key_)); // 使用密鑰對每個元素做XOR

//加密後結果:  ["10100111", "10010110", "10000000", "10000111"]

然後進行解密

//解密 (等於是把加密後的結果在與KEY做一次XOR)
(["10100111", "10010110", "10000000", "10000111"]).map(d => XOR(d, key_));

//解密後結果 ["01010100", "01100101", "01110011", "01110100"]

// 最後轉為字串
(["01010100", "01100101", "01110011", "01110100"]).map(d => String.fromCharCode(parseInt(d, 2))).join('');
// 得到字串:Test

Stream Ciphers 與 Block Ciphers

對稱式加密可以使用Stream Ciphers或是Block Ciphers將明文預先進行分段後再進行演算法加密來提高安全性,以及對於不同加密需求時可以使用不同模式 ( 例如當明文長度大於 AES 的 128 bits 限制時,可以用區塊加密將其切割 )。

可以在Node.js使用 crypto.getCiphers() 來查看可用之加密方法。

1.Stream Ciphers

Stream Ciphers ( 資料流加密 ) 並不會將明文切分為區段,而是一次加密一個資料流的位元或是位元組。常見的作法是將 較短的加密鑰匙延展成為無限長、近似亂碼的一長串金鑰串流 ( keystream ),再將金鑰串流和原始資料 ( plaintext )經過XOR運算後,產生密文資料 ( cipher text )。

Stream Ciphers 被用在視訊串流或行動通訊等領域。使用Stream Cipher 之加密演算法例如: A5/1、RC4

由於它可以用硬體來實作,在以前CPU運作速度不夠快的時代比較流行。

1.金鑰長度無法和串流明文一樣長
2.金鑰產生金鑰串流
3.金鑰串流不能重複

2.Block Ciphers

Block Ciphers ( 區段加密 ) 會將明文分成數個n個字元或位元的區段,並且對每一個區段資料使用相同的演算法和密鑰加密。

一開始會把明文先拆開成區塊,然後第一個區塊先進行加密演算法後得到密文,之後這個密文再繼續跟後續區塊做計算。

假設M為明文,其將分割成M1、M2… Mn區段, 然後K為密鑰,其可表示為 : E(M,K)=E(M1,K).E(M2,K)……E(Mn,K)

較適合對已知加密檔案內容的檔案進行加密。

參考至:https://security.stackexchange.com/a/345

以下先介紹比較早期發展的四種區塊加密方式:ECB、CBC、CFB和OFB

ECB

最簡單的加密模式即為(Electronic codebook,ECB)模式。需要加密的訊息按照設定的大小被分為數個區塊,並對每個區塊進行獨立加密。但缺點在於同樣的明文區塊會被加密成相同的密文區塊,並且重複使用同一把金鑰,因此並不推薦用於現代的加密協定中。

https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation

過程主要為

將要加密的明文 ( plaintext ) 分為多個區塊,並用同一支金鑰進行加密的動作。

CBC

全名為Cipher-block chaining,為區塊加密中最常用的模式,在CBC模式中,每個明文區塊先與前一個密文區塊進行XOR後,再進行加密,也就是將前一個區塊加密後的結果作為加密第二個區塊的輸入。在這種方法中,每個密文塊都依賴於它前面的所有明文區塊。並且,其會要求輸入一個初始向量,該初始向量會先跟第一個明文區塊做XOR。

主要缺點在於加密過程是依序進行的,無法同時進行,所以速度較慢,並且加密訊息必須被填充到區塊大小的整數倍,但其解密是可以並行運算的。

其加密過程如下圖

https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation

1. 初始向量 IV (註2) 與 Plaintext 做 XOR 之後再和金鑰進行演算法加密
2. 然後第上一步驟的結果與下一個區塊的 Plaintext 做 XOR 之後再和金鑰進行演算法加密
3. 重複上一步驟直到所有內容加密完成

CFB

類似剛才提到的CBC模式,前一部分的明文改變將會影響到接下來加密出的密文,其將區塊加密變為可自行同步的串流加密法,如果部分的cipher text在傳輸過程遺失,也不會導致所有原始訊息遺失,仍然可以繼續解密出剩下的訊息,且訊息無需進行填充到一定長度,

其加密過程如下圖

[https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation](https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_Feedback_(CFB)

過程大致如下

1. IV 直接加入金鑰並進行演算法加密
2. 將第一步驟的結果與 plain text 進行 XOR 的運算
3. 將第二個步驟所產生的結果作為下一回合的輸入
4. 重複上一步驟直到所有內容加密完成

OFB

全名為(Output feedback, OFB)可以將區塊加密變成同步的串流加密法。IV與Key產生出block cipher encryption後與明文進行XOR,得到密文。與其它串流加密法一樣,密文中一個 bit 的翻轉會使明文中同樣位置的bit 也產生翻轉。這種特性使得許多錯誤校正碼,例如奇偶校驗位,在加密前計算與加密後進行校驗均可得出正確結果。

由於XOR操作的對稱性,加密和解密操作是完全相同的:

https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation

1. IV 直接加入金鑰並進行演算法加密
2. 將第一步驟的結果與 plain text 進行 XOR 的運算 
3. 將第一步驟所產生的結果作為下一回合的輸入
4. 重複上一步驟直到所有內容加密完成

CTR

稱為Counter mode,CTR模式也稱為計數器模式,每個區塊對應一個累加的計數器,通過計數器來生成加密密鑰串流,計數器可以是任意保證長時間不產生重複輸出的函數。下圖的Nonce加上Counter當做一個改變的數值,Nonce和前面幾種模式的IV類似,每次加密都需要隨機生成。而計數器Counter是累加的。CTR模式特點是每組加密都是獨立的,不依賴前一組,所以可以進行並行計算。

在計數器模式下,我們先對一個累加的計數器進行加密,再用加密後的bits序列與明文分組進行 XOR 得到密文。

https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation

另外有兩種區塊加密法與CTR相關

1. GCM: GCM中的G就是指GMAC,C就是指上述的CTR。
2. CCM: CCM為CMAC認證算法結合CTR。

而四種方式的比較與選擇可參考:https://stackoverflow.com/a/1220869

Node.js中的對稱式加密相關函式

主要包含

crypto.createCipher()
crypto.createCipheriv()

其中createCipher是輸入密碼,而createCipheriv需輸入key與iv值

例如cbc等需要iv的也可以用createCipher並輸入密碼,因為它會自動衍生出iv與key參考下面程式

https://gist.github.com/bnoordhuis/2de2766d3d3a47ebe41aaaec7e8b14df


註1: XOR

( 兩兩數值相同為否,而數值不同時為真 ): 例如1 xor 1為 0 , 1 xor 0 為 1

F(false)亦代表0,T(true)亦代表1

至於為何要使用XOR在對稱式加密,不使用AND或OR,可參考:https://stackoverflow.com/a/24731845

註2: IV 初始向量

通常是一個亂數,讓攻擊者難以對同一把金鑰產生的密文進行破解,會加入區塊加密中一起進行加密運算。

https://zh.wikipedia.org/wiki/初始向量

AES

Advanced Encryption Standard,縮寫:AES,又稱Rijndael加密法,但嚴格來說AES和Rijndael加密法並不完全一樣,因為Rijndael加密法可以支援更大範圍的區塊長度,AES的區塊長度固定為128位元而Rijndael使用的區塊長度可以是128,192或256位元。AES用來替代原先的DES。

AES的區塊長度固定為128位元,金鑰長度則可以是128,192或256位元;而Rijndael使用的金鑰和區塊長度均可以是128,192或256位元。

AES加密過程是在一個4×4的位元組矩陣上運作,其初值即為一個明文區塊,矩陣中一個元素大小就是明文區塊中的一個Byte。

其加密方法主要包含四個步驟

1. AddRoundKey: 將每個狀態中的位元組與該回合金鑰做異或(⊕)。
2. SubBytes:    矩陣中各位元組被固定的8位元尋找表中對應的特定位元組所替換。
3. ShiftRows:   矩陣中每一行的各個位元組循環向左方位移。位移量則隨著行數遞增而遞增。
4. MixColumns:  每個直行與 AES 定義的多項式 c(x) 進行多項式乘法。

以上參考至: https://en.wikipedia.org/wiki/Advanced_Encryption_Standard

AES-256 範例

const crypto = require('crypto');

const mode = 'aes256' // 可更換為aes128或aes192等等

// 加密
const cipher = crypto.createCipher(mode, 'a password');
let encrypted = cipher.update('I_am_plaintext', 'utf8', 'hex');
encrypted += cipher.final('hex');
console.log(encrypted);

// 解密
const decipher = crypto.createDecipher(mode, 'a password');
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
console.log(decrypted);

AES之區塊加密模式

在CBC、OFB、CFB、CTR等區塊模式 IV 長度均為 16 bytes,GCM模式的 IV 則沒有一定要 16 bytes

AES-256-CBC範例

const crypto = require('crypto');

// 因為aes-256要求之key 長度為256bits 也就是32 bytes = 32個ASCII英文字母
// aes-128 要求之key 長度為128bits 也就是16 bytes = 16個英文字母
let key = Buffer.from("abcbbbbbbbbbbbbbabcbbbbbbbbbbbbb", 'utf-8') //or crypto.randomBytes(32);

const IV_LENGTH = 16; 

function encrypt(text) {
    let iv = crypto.randomBytes(IV_LENGTH);
    // 可直接替換為ofb、cfb、ctr等模式
    let cipher = crypto.createCipheriv('aes-256-cbc', new Buffer(key), iv);
    let encrypted = cipher.update(text);
    encrypted = cipher.final();

    // 將IV附上 在解密時須告知
    return iv.toString('hex') + ':' + encrypted.toString('hex');
}

function decrypt(text) {
    let textParts = text.split(':');
    let iv = new Buffer(textParts.shift(), 'hex');
    let encryptedText = new Buffer(textParts.join(':'), 'hex');
    let decipher = crypto.createDecipheriv('aes-256-cbc', new Buffer(key), iv);
    let decrypted = decipher.update(encryptedText);

    decrypted = decipher.final();

    return decrypted.toString();
}

console.log(encrypt("test"));
console.log(decrypt(encrypt("test")))

AES-256-GCM範例

需要加上cipher.getAuthTag(); 與 decipher.setAuthTag();

目前 AuthTag 與 Additional Authenticated Data ( AAD ) 在 Node.js 只有 GCM 模式支援,cipher.setAAD需要在 update() 之前使用,而 cipher.getAuthTag() 必須要在cipher.final()執行後才能使用。

const crypto = require('crypto');


const mode = 'aes-256-gcm';
// 因為aes-256要求之key 長度為256bits 也就是32 bytes = 32個ASCII英文字母
// aes-128 要求之key 長度為128bits 也就是16 bytes = 16個英文字母
let key = Buffer.from("abcbbbbbbbbbbbbbabcbbbbbbbbbbbbb", 'utf-8') //or crypto.randomBytes(32);

const IV_LENGTH = 12;
let tag;

function encrypt(text) {
    let iv = crypto.randomBytes(IV_LENGTH);

    let cipher = crypto.createCipheriv(mode, new Buffer(key), iv);
    let encrypted = cipher.update(text, 'utf8', 'hex');
    encrypted += cipher.final('hex');
    // 需要加上AuthTag
    tag = cipher.getAuthTag();
    // 將IV附上 在解密時須告知
    return iv.toString('hex') + ':' + encrypted.toString('hex');
}

function decrypt(text) {
    let textParts = text.split(':');
    let iv = new Buffer(textParts.shift(), 'hex');
    let encryptedText = new Buffer(textParts.join(':'), 'hex');
    let decipher = crypto.createDecipheriv(mode, new Buffer(key), iv);
    // 需要加上AuthTag
    decipher.setAuthTag(tag);
    let decrypted = decipher.update(encryptedText, 'hex', 'utf8');
    decrypted += decipher.final('utf8');

    return decrypted.toString();
}

console.log(encrypt("test"));
console.log(decrypt(encrypt("test")))

其他Node.js的相關範例可參考

https://github.com/nodejs/node-v0.x-archive/blob/master/test/simple/test-crypto-authenticated.js#L44-L64

OpenSSL範例

產生範例檔案

echo test > file.txt

加密(執行後會要求輸入密碼)

openssl enc -aes-256-cbc -salt -in file.txt -out file.txt.enc

解密(執行後會要求輸入剛才加密的密碼)

openssl enc -aes-256-cbc -d -in file.txt.enc -out result.txt
7 回复

@EasonWang01 能否把内容也放到正文里,这样方便加精华

好的,目前已加上內文。

好厉害! HMAC 的 secret 和 salt 有什么区别呢? 有些疑惑

回到顶部