Categories: python

python-rsa: encryption and decryption with your ssh keys

可不可以用自己的一組 ssh keys 來加密解密文件呢?

除了 gnupg 的另外一種選擇

有些資料不想直接寫進檔案裡

像是密碼更甚至是一些個人隱私重要的資訊像是身份證字號等等的資料,直接寫進檔案裡紀錄起來總是覺得令人擔心,萬一被別人看到了呢?也許還會用版本控制來管理,即使很小心不要公開,但萬一真的被複製了呢?最簡單避免這種不幸的狀況發生的方法就是只寫自己才知道的關鍵字,像是密碼也許就紀錄前後字母就可以了,或是身份證的話就只寫入後面幾碼,這樣一來即便檔案被拿走,可能還需要配合其他的個資才會有真正的損害,如此可以達到基礎的保護效果,大概可稱之為「人腦加密」。

但年紀越大記憶力越來越差,偏偏現在要求的密碼規則越來越複雜,即便有提示但可能也很難都記住相關的轉換,放網路服務雖然也不錯,但自己的密碼隱私資訊自己沒有留一份好像就不是真正的擁有。那有哪些機制可以用呢?另外一個簡單的方法就用像是 vim -x file.txt 一樣 ,要先設定一組密碼才能讀寫這個檔案。

看起來不錯但實際用起來有點麻煩,也許整個文件裡面只有部份需要保護,其他被看到也沒關係的,但為此都需要整份檔案解開才行,還常常需要輸入密碼。而把整個文字檔變成了 binary 格式更是增加版本控制的困擾,牽一髮而動全身,可能還需要用 git lfs 來管理才行。


明明就有 gnupg 為什麼還想要用 ssh 的 keys 呢?

其實在 email 上也有加密解密郵件的需求,要怎麼確定這封信是寄給這個人的呢?可以把自己建立好的 public key 上傳到 gpg server 上,別人就可以用上面的 public key 寫一封專屬的 email 給這個人,有相對應的 private key 的人才能正常解開讀到這封信。

不過至少我個人已經不太用 gnupg 了,然後明明只想做一點點加密的保護,可能還要上傳個 public key 到 gpg server 上好像也太大費周章,比較起來每天都在使用 ssh 也有 public/private keys 機制,那有沒有辦法就直接使用現成的 ssh 的 keys 來做這樣的事情呢?

附帶一提的是現在的版本控制多半可以讓使用者上傳 ssh 的 public key ,這樣就不用輸入密碼可以直接操作。但有時候年代久遠之前上傳過的 public key 到底是不是這台電腦上的呢?也許前面的名稱都一模一樣,通常也都沒有辦法看到完整已經上傳的 public key ,只有像是如下面的 fingerprint 資訊而已,這樣該怎麼判斷呢?

Yumao.Kao@nb-YumaoKao 8c:7c:93:6c:03:49:87:74:7c:e7:bc:b6:88:37:f2:cf
last used: 1 week ago

其實這些 fingerprints 可能是做過 MD5,可以將手上有的 public key 做一樣的事情來比對看看就可以了。想起來之前算常遇到有人跑來問為什麼不能 clone ,可能的原因是之前在不明究理之中重新產生一對了而沒有更新。

$ ssh-keygen -l -E md5 -f $HOME/.ssh/id_rsa.pub
2048 MD5:8c:7c:93:6c:03:49:87:74:7c:e7:bc:b6:88:37:f2:cf Yumao.Kao@nb-YumaoKao (RSA)

純 python 的 rsa 實做 python-rsa

看起來使用 ssh 的 key pairs 是可行的,還可以自行管理 public key 放置地方,只要能夠正常讀取到 keys 然後可以成功加密解密就可以了。那麼要怎麼挑選加解密的工具呢?考量過的幾點需求:

  • 最好能跨平台,相依性越小越好。
  • 最好能直接讀 ssh-keygen 的 (RSA) keys。
  • 不用可以加密整個大檔案,最好可以以字串或是 bytes 的方字傳進傳出。

看過了幾個之後,決定使用 python-rsa 這個 python 的模組,主要的原因是它是純 python 實做的,所以只要有 python 就可以直接裝上,不用擔心可能沒有某個 os wheel 的支援。而看起來 api 設計也算簡單直覺,可以直接傳 bytes array 進出,容易接上後面的應用。

不過還剩一個問題,要怎麼讀進 ssh 的 keys 呢?直接把檔案傳進去看起來無論是 public key 還是 private key 都沒有辦法正常讀到,需要做一些轉換才能正常讓 python-rsa 讀到,所以先來看一下可以支援的格式 PKCS#1PKCS#8

PEM PKCS#1

-----BEGIN RSA PRIVATE KEY-----
BASE64 ENCODED DATA
-----END RSA PRIVATE KEY-----

-----BEGIN RSA PUBLIC KEY-----
BASE64 RSA ENCODED DATA
-----END RSA PUBLIC KEY-----

PEM PKCS#8

-----BEGIN RSA PUBLIC KEY-----
BASE64 ENCODED DATA
-----END RSA PUBLIC KEY-----

-----BEGIN RSA PRIVATE KEY-----
BASE64 ENCODED DATA
-----END RSA PRIVATE KEY-----

python-rsa 的文件說明中,只有明確寫到支援 PKCS#1 格式,但神奇的地方是 rsa 的文件裡,PublicKey.load_pkcs1_openssl_pem() 說是可以讀 PKCS#8 這種格式的,而且不知道為什麼只有 public key 有,那不如就來試試看吧。

當然首先要先裝一下 python-rsa

$ pip install rsa
# Or if you use arch
$ yay -S python-rsa

接著產生一組新的 ssh keys in RSA 並且用來轉成可支援的格式,

有了 private key 就可以產生回 public key 的喔

# generate a new rsa key pair with comment
$ ssh-keygen -f id_rsa -C for-python-rsa

# copy and transform in place private key
$ cp id_rsa id_rsa.pkcs1
$ ssh-keygen -m pem -f id_rsa.pkcs1 -p

# export public key in pkcs#8 format
$ ssh-keygen -f id_rsa -e -m pkcs8 > id_rsa.pub.pkcs8

就可以來寫個自己加密解密的小程式,並且可以看到預期的結果。

import pathlib
import rsa

def main():
     privkey_path = pathlib.Path() / 'id_rsa.pkcs1'
     with open(privkey_path, 'rb') as f:
         privkey = rsa.PrivateKey.load_pkcs1(f.read())

     pubkey_path = pathlib.Path() / 'id_rsa.pub.pkcs8'
     with open(pubkey_path, 'rb') as f:
         pubkey = rsa.PublicKey.load_pkcs1_openssl_pem(f.read())

     message = 'with pubkey in pkcs#8 short string'.encode('utf8')
     crypto = rsa.encrypt(message, pubkey)
     dec_msg = rsa.decrypt(crypto, privkey)
     print(dec_msg.decode('utf8'))
$ python encdec.py
with pubkey in pkcs#8 short string

延伸:yaml 的 binary type

雖然用 python-rsa 可以達成需求了,但上面的例子加密完的 crypto 好像都只有存在記憶體裡耶,跑完就不見了那該怎麼保存比較好呢?又 crypto 是個 物件,直接寫成檔案是可以,但有沒有辦法變成文字檔的一部分呢?

是可以的,而且透過 yaml 的 binary 形式還能夠更方便的達到,讀回來起來就是 bytes 了。根據文件說明 yaml 的 binray type 需要用 base64 格式儲存,所以先把剛剛的 crypto 轉成 base64 的字串。

import base64

crypto = rsa.encrypt(message, pubkey)
print(base64.b64encode(crypto))

接著就可以用 !!binary 寫到 yaml 檔案裡面,最後再讀回來就直接可以送去 decrypt() 了喔。

crypto: !!binary |
    OWhPxBTcA4f6oDVWVAsTx77xHjnxwjl1QFEBNKYIZ0LPhZkWwy
    MCX9zClbHZ+QFWjjeoKk2q6BTDOS0/fUQ99G32liJopXn4f4nQ
    sH70rPV8YDD/lpoVu1yHcuV5ZPtd8uZoEYck4iKcpIFH7J3JXY
    d4EMgr1c/CCvPyUEV/lTQmUkZsxj308RaZ/BXiuju+34mP9sO/
    cnolKAFgcKq/5HckpXgf9OXkP9lscHbBJlB8jEa35Hk3I4nPhJ
    d6ZXfY5++AJjjTh3i1eNGGuud/CvN8JshKyV1FbgSSjgH5GHHc
    ALWusc3/nBBpNf5/WEmPr4DyOgmicLfDSA4zuykXcKBAc2OiKW
    bpKgEVp/zrPPIzSmiE3cI/DD5pbKKtrPLOLu3ZVHcgy+iKAWOK
    f8iJWL8RpVzHqGXybbBdI3DtGervlNpgMXu20x9pPm7sEZs+XJ
    H4FE8k5muDgdahX4mjlLBEPTzQP85ITYnRq7ONqozJxzmiblA0
    Tkclznoa
yumaokao

Recent Posts

Invoke Method with JDWP

smali breakpoin...

2 年 ago

tetris3d with plotly

魔術磁力方塊好好玩 魔術磁力方...

3 年 ago

AliasDict, a python dict for alias resolver

用 dict 存 alias ...

3 年 ago

hashcat sha256 from mac address

只給一天期限的憑證檔案 去年幫...

3 年 ago

python rich with asyncio

python rich 畫圖很...

3 年 ago