2015年1月20日 星期二

Ruby, Product Key, 使用Ruby自製產品序號,獎品序號

製作產品序號要多複雜可以搞得多複雜,所以這篇文章以最簡單的AES加密,並用BASE32編碼為英數字為例。原理相同,之後想搞得多複雜可以慢慢做

關於AES,與Base32。我將使用以下步驟來產生產品序號

  1. 規劃要產生的文字,譬如客戶輸入序號,得到獎品為紅藥水10瓶,就簡單的使用potion10這串文字來編成序號。
  2. 使用AES編碼產生類似U\xDB\e\xDC\xA2S$\xE7p\x13\x85\xD3'\x00\x9B\x10encryption block
  3. 使用Base32將上面那堆醜醜的東西,編碼為英文與數字的組合,類似KXNRXXFCKMSOO4ATQXJSOAE3CA======

然後就可以將KXNRXXFCKMSOO4ATQXJSOAE3CA======送給你的客戶,當他輸入獎品序號後,在解碼回來就行了。後面那個====,可以不用給客戶,你的後台收到客戶輸入時,幫他加上去就行了。

解碼回來得到字串potion10就給那位客戶10罐紅藥水,若解碼回來的字串沒有意義,就代表那是隨意輸入的序號,紀錄他嘗試錯誤的次數在log,要注意有人在亂try。

至於=符號的數量代表什麼意思,可以參考這篇文章Base32 Encoding Algorithm

中間有一段

The Base32 encoding process is to:

Divid the input bytes stream into blocks of 5 bytes.

Divid 40 bits of each 5-byte block into 8 groups of 5 bits.

Map each group of 5 bits to 1 printable character, based on the 5-bit value using the Base32 character set map.

If the last 5-byte block has only 1 byte of input data, pad 4 bytes of zero (\x0000). After encoding it as a normal block, override the last 6 characters with 6 equal signs (======).

If the last 5-byte block has only 2 bytes of input data, pad 3 bytes of zero (\x0000). After encoding it as a normal block, override the last 4 characters with 4 equal signs (====).

If the last 5-byte block has only 3 bytes of input data, pad 2 bytes of zero (\x0000). After encoding it as a normal block, override the last 3 characters with 3 equal signs (===).

If the last 5-byte block has only 4 bytes of input data, pad 1 byte of zero (\x0000). After encoding it as a normal block, override the last 1 characters with 1 equal sign (=).

Carriage return (\r) and new line (\n) are inserted into the output character stream. They will be ignored by the decoding process.

根據我的測試,只要選用相同的AES加密模式(譬如我下面會用128 bit, CBC模式),產生的=號數都相同,所以可以當作是常數,直接寫在程式碼裡面,BASE32編碼後拿掉,解碼後加回來,就可以了。

我以Ruby來實作,Ruby本身已經實做了OpenSSL相關功能,而Base32的實作,可以參考Base32

以下是我已經在電腦安裝Base32的情況下,來實作序號這個功能(若還沒安裝可以gem install --remote base32來安裝)

require 'openssl'
require 'base32'

data = 'potion10'   

cipher = OpenSSL::Cipher::AES128.new(:CBC)
cipher.encrypt
key = cipher.random_key
iv = cipher.random_iv
encrypted = cipher.update(data) + cipher.final

encrypted_base32 = Base32.encode(encrypted)
#=> "N4AGAVVX34E3W6KSANFVGYG4IM======"
#   我們取"N4AGAVVX34E3W6KSANFVGYG4IM"這個部分給客戶輸入就好
#   解密時,把後面5個'='號給補上就好
#   如果你選用的key, iv產生的'='數目不同,則自行改變

解密的話,就把步驟反過來,先用Base32解碼,再解密,不過要用同一組key跟iv(關於iv是什麼,AES加解密過程中使用的一個陣列,詳細請看塊密碼的工作模式)

假設剛剛加密時,使用的key是U\x94\xCA[\x00\xD1\x06\x81\x8C\xA7eU<\xAC\x16\xC9,使用iv是\x03\xFA\xBDe\x81$\xD1\xF20\x18%[lA\xC7\x15

通常這些資料會寫在環境變數裡面,然後在ruby裡面使用ENV[key],ENV[iv]取出來比較安全。就像Rails裡面secrets.yml檔案讀取<%= ENV["SECRET_KEY_BASE"] %>相同的道理,詳參閱config/secrets.yml

所以我們先把key與iv放在環境變數裡面,在console裡面打上

AES128_CBC_KEY='U\x94\xCA[\x00\xD1\x06\x81\x8C\xA7eU<\xAC\x16\xC9'
AES128_CBC_IV='\x03\xFA\xBDe\x81$\xD1\xF20\x18%[lA\xC7\x15'
export AES128_CBC_KEY
export AES128_CBC_IV

AES128_CBC_KEYAES128_CBC_IV設定好,並export為環境變數

因此,最上面那個加密並編碼的程式就可以改成這樣,我們直接寫成Method的形勢,接受的參數是要加密的序號

require 'openssl'
require 'base32'

def encrypt_to_serial data

    cipher = OpenSSL::Cipher::AES128.new(:CBC)
    cipher.encrypt

    # 差別在此,直接從環境變數取得key與iv
    # 之後解密時,也直接從環境變數取得key與iv
    cipher.key = ENV['AES128_CBC_KEY']
    cipher.iv = ENV['AES128_CBC_IV']

    encrypted = cipher.update(data) + cipher.final
    encrypted_base32 = Base32.encode(encrypted)

    # 本例所選AES模式 編碼後,後面會有6個等號,所以回傳[0..-7]
    return encrypted_base32[0..-7]
end

這樣子,我們的的解密程式,也就可以簡單的依序反向操作,也不用擔心kev與iv要如何取得的問題

require 'openssl'
require 'base32'

def decrypt_from serial

    # 同樣的 本例是因為後面要補回6個'='號
    encrypted_base32 = serial << "======"
    encrypted = Base32.decode(encrypted_base32)

    decipher = OpenSSL::Cipher::AES128.new(:CBC)
    decipher.decrypt
    decipher.key = ENV['AES128_CBC_KEY']
    decipher.iv = ENV['AES128_CBC_IV']

    return decipher.update(encrypted) + decipher.final
end

以上做法是比較簡單明顯的做法,實務上可以考慮寫成class來使用

沒有留言:

張貼留言