توسعه دهنده و مهندس شبکه
برسی الگوریتم رمزنگاری rabbit و پکیج rabbitio در گولنگ
این مطلب از وبلاگ توسعه دهندگان SNIX کپی برداری شده است... لینک پست درblog.snix.ir
توی این دوره زمونه رمزنگاری اطلاعات یکی از مهم ترین بخش های دنیای دیجیتال هست، البته اهمیت رمزنگاری اطلاعات و استفاده از اون حتی به قبل از دنیای دیجیتال برمیگرده، شاید این جمله رو شنیده باشید که میگه information is power .. قطعا ارزش اطلاعات در هر برهه ای از تاریخ حائز اهمیت بوده، از زمان امپراطوری روم تا جنگ جهانی اول و انیگما در جنگ جهانی دوم و دوران جنگ سرد میتونیم اهمیت رمزنگاری اطلاعات و نقش اون توی تغییر سرنوشت بشریت رو مشاهده کنیم.
قطعا رمزنگاری توی دنیای دیجیتال و خصوصا با گسترش اینترنت نقش خیلی پررنگ تری نسبت به قبل ایفا میکنه. برای مثال حتی همین پستی که شما دارید میخونید رمز شده.
ولی رمزنگاری چیه؟ رمزنگاری در دنیای دیجیتال به معنی ناخوانا کردن اطلاعات برای افراد ثانویه با استفاده از الگوریتم های ریاضی هست. به صورتی که فرستنده و گیرنده بتونن این اطلاعات رو با یک یا چند کلید رمز و رمزگشایی کنن.
تا به امروز قالب بر چند صد الگوریتم مختلف رمزنگاری منتشر شده که هرکدوم مزیت ها، مشکلات و نقص های امنیتی خودشونو دارن. توی این پست میخوایم الگوریتم رمزنگاری rabbit یا خرگوش رو مورد بررسی قرار بدیم.. به صورت خلاصه نحوه رمزنگاری اطلاعات توی این الگوریتم رو مرور کنیم و با یکی از الگوریتم های دیگه از نظر امنیت مقایسش کنیم و در نهایت پرفورمنس rabbit رو اندازه گیری کنیم.
الگوریتم rabbit یا rabbit cipher (به الگوریتم رمزنگاری cipher میگن) در سال 2003 ارائه و در سال 2008 برای استفاده عموم منتشر شد. این الگوریتم که از نوع stream cipher و symmetric هست از اولش برای افزایش امنیت اطلاعات ارائه شد اما بعدا مشخص شد که علاوه بر امنیت، سرعت بسیار بالایی هم داره. قبل از اینکه ادامه بدیم:
منظور از stream cipher چیه: همونطور که از اسمش پیداست به این معنیه که اطلاعات بایت به بایت رمز میشن.. برخلاف block cipher که یک بلوک از اطلاعات رو (مثلا 64 بیت) رمز میکنه
منظور از symmetric چیه: الگوریتم های رمزنگاری از نظر نوع کلید به دو دسته symmetric و asymmetric دسته بندی میشن. symmetric ها برای رمزگذاری و رمزگشایی از یک کلید استفاده میکنن مثله rabbit و asymmetric ها برای رمزگشایی از private key و برای رمزگذاری از public key استفاده میکنن مثله RSA
مفهوم plain-text: اطلاعاتی که رمز نشده هستند و برای همه قابل خواندن هست
مفهوم cipher-text: اطلاعاتی که توسط یک cipher رمز شده و برای همه قابل خواندن نیست
مزیت رمزنگاری symmetric:
- بسیار سریع هستن
- بسیار امن هستن ( شکستن دیتا با استفاده از حمله Brute-force مدت زمان زیادی طول میکشه)
- طول cipher-text مساوی یا کمتر از plain-text هست
مزیت رمزنگاری asymmetric:
- کلید خصوصی به اشتراک گذاشته نمیشه
- فرستنده و گیرنده و پیام تایید هویت میشه
در پروتکل TLS از هردو روش استفاده میشه... برای تایید هویت از RSA یا DSA و برای رمزنگاری از AES یا DES و جدیدا با chacha20
نحوه رمزنگاری اطلاعات در الگوریتم rabbit: این الگوریتم برای رمزنگاری از یک کلید 16 بایتی و Initialization vector هشت بایتی استفاده میکنه. Initialization vector اختیاری هست. توی ورژن های اولیه Initialization vector وجود نداشت و به منظور امنیت بیشتر اضافه شد.
این Initialization vector چیه: بعضیا بهش تاس هم میگن.. معمولا یک مقدار تصادفی هست که به منظور افزایش امنیت ازش استفاده میشه، به طور خلاصه کاری که انجام میده اینکه که اطمینان حاصل میکنه که cipher-text های تولید شده از یک plain-text شبیه به هم نباشن... مثال: فرض کنید hello-world رو با کلید secret-key رمز کردید.. اگه لازم باشه اینو چند بار روی یک کانکشن ارسال کنید، حمله کننده مرد میانی (شنود کننده اطلاعات یا فرد ثانویه) از متن پیام که hello-world هست با خبر نمیشه اما قطعا میفهمه که شما دارید یک plain-text ثابت رو چند بار ارسال میکنید. نقش iv اینجا مطرح میشه.. با استفاده از iv هر cipher-text تولید شده از یک plain-text ثابت با استفاده از کلید با مقدار قبلیش فرق میکنه. اگه از iv استفاده کنید باید با هر cipher-text این iv هم ارسال بشه.. البته ارسال این iv و مطلع شدن فرد ثانویه از این iv ها هیچ خطری نداره.
امنیت اطلاعات در rabbit: از اونجایی که rabbit از کلید 128 بیت استفاده میکنه شکستن اطلاعات به روش brute-force مدت زمان غیر قابل تصوری طول میکشه.. برای مثال کل شبکه بیت کوین رو درنظر بگیرید، شکستن کلید rabbit برای این شبکه 70,000,000,000,000,000,000,000,000 سال طول خواهد کشید. حتی برای تکنولوژی های جدید اینده مثل کامپیوتر های کوانتومی هم شکستن این کلید به مدت زمان 12^10*2.61 سال طول میکشه ... برای اینکه بزرگی این مدت زمان ها دستتون بیاد لازمه بدونید کل سن جهان هستی از زمان بیگ بنگ تا الان تقریبا 10^10*1.38 تخمین زده شده...
نحوه رمزنگاری اطلاعات: اندازه internal state این سایفر 513 بیت هست که شامل 8 متغیر 4 بایتی state و 8 متغیر 4 بایتی counter و یک بیت به عنوان carry میشه.
- تابع setup key: این تابع کلید 16 بایتی رو به 8 ارایه 2 بایتی تبدیل میکنه و با این ارایه ها متغیر های state و counter محاسبه میشن.
- به منظور کاهش ارتباط بین کلید و متغیر های internal state فانکشن next state چهاربار اجرا میشه..
- تابع setup iv: این تابع مقدار iv رو که 8 بایت هست با متغیر های counter که 8 بایت هستند xor میکنه
- بعد از این 4 بار دیگه فانکشن next state اجرا میشه تا متغیر های counter جدید با state ها محاسبه مجدد بشن.
- در فانکشن next state متغیر های counter هر بار با بیت carry و ثابت های rabbit (هشت ثابت چهار بایتی) sub32 میشن.. تابع sub32 برای محاسبه difference پارامتر اول، دوم و سوم استفاده میشه. توی این حالت پارامتر اول فیلد ارایه ثابت هاست و پارامتر دوم فیلد متناظر ارایه counter ها و پارامتر اخر هم carry هست..
- تابع extract: توی این تابع حالت های مختلف متغیرهای state با همدیگه xor میشن.. که در نهایت باعث تولید key stream میشه..
- رمزگذاری و رمزگشایی: هر بایت از key stream که اسلایسی به طول 16 هست با هر بایت از plain-text یا همون دیتای رمز نشده xor میشه و اون بایت از key stream حذف میشه... در نهایت وقتی طول key stream به صفر رسید دوباره extract انجام میشه
برای اطلاعات بیشتر میتونید به RFC 4503 و پروپوزال رسمی ارائه شده مراجعه کنید.
پروژه eSTREAM و rabbit: پروژه eSTREAM که توسط EU ECRYPT (سازمان تحقیقاتی امنیت اطلاعات و رمزنگاری اتحادیه اروپا) حفظ میشه هدفش پیدا کردن و ارائه دادن cipher stream های بهینه و امن برای استفاده عمومی و گسترده هست. rabbit فاز سوم این پروژه رو پشت سر گذاشته و به همراه HC-128 و Salsa20/12 و SOSEMANUK به عنوان کاندید های نرم افزار مورد تایید قرار گرفته.
مقایسه rabbit با الگوریتم های ضعیف تر، encrypt تصویر... : برای مقاسه من سایفر AES-ECB رو انتخاب کردم. و میخوام باهاش یک تصویر رو encrypt و نمایش بدم...
چون میخوام تصویر رمز شده رو نمایش بدم پس نباید header های فایل رو رمز کنم... اگه کل فایل png رو رمز کنم header ها هم رمز میشن و فایل دیگه با اپلیکیشن های مشاهده تصویر باز نمیشه.. چون هدر ها هم تغییر کردن.. برای مشاهده تصویر رمز شده باید فقط اطلاعات مربوط به نحوه رنگ دهی پیکسل ها encrypt بشه.. البته png سختار پیچیده ای داره و من برای سادگی کار تصویر png رو به فورمت ppm که نسبتا ساده تر هست تبدیل میکنم...
فرمت تصویر ppm: این فرمت شامل هدر و به دنبال اون دیتا میشه .. توی ورژن P3 هدر به صورت P3 1024 788 255 تعریف میشه و از لاین بعد کد رنگ هر پیکسل به صورت ascii تعریف میشه.. مثلا اگه این کد رو توی فایل text کپی کنید و با پسوند ppm ذخیره کنید، تصویر 3 در 4 پیکسل رو میتونید ببینید.
P3 3 4 255
255 0 0 0 255 0 0 0 255 255 255 0 255 255 255 0 0 0 0 255 255 75 75 75 127 127 127 150 150 150 150 150 150 150 150 150
توی ورژن P6 این فرمت فایل اطلاعات به صورت باینری توی فایل ذخیره میشن.. که کار رو برای encrypt کردن دیتا راحت تر میکنه.. پس فایل png رو تبدیل به ppm P6 میکنم.
خب من با دستور convert از پکیج imagemagick عکس png رو تبدیل به ppm میکنم:
# apt install imagemagick
# convert logo.png logo.ppm
واسه اینکه بفهمم طول هدر چند بایت هست این فایل logo.ppm رو با دستور xdd یا hexdump برسی میکنم..
# hexdump -C logo.ppm | head -n 3
00000000 50 36 0a 33 38 34 20 33 38 34 0a 32 35 35 0a 00 |P6.384 384.255..|
00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
پس طول هدر این عکس 15 بایت هست و باید این 15 بایت رو از فایل جدا کنم و فایل رو با الگوریتم رمز کنم و دوباره هدر رو به اطلاعات رمز شده اضافه کنم..
عکسی که انتخاب کردم اینه ...
خب برای رمزنگاری این فایل با سایفر AES-ECB میخوام از openssl استفاده کنم و برای رمزنگاری با rabbit از کتابخونه snix.ir/rabbitio و زبان go استفاده میکنم.. این پکیج رو میتونید از git.snix.ir یا میرور گیت هاب دریافت کنید
برای جدا کردن 15 بایت اول فایل logo.ppm توی لینوکس از دستور dd کمک میگیرم:
# dd if=logo.ppm bs=1 count=15 of=header.bin
15+0 records in
15+0 records out
15 bytes copied, 0.000500669 s, 30.0 kB/s
هدر رو توی فایلی به اسم header.bin ذخیره کردم... اما فایل logo.ppm هنوز header رو داره .. پس باید هدرش رو پاک کنیم که فقط اطلاعات رنگ بندی RGB باقی بمونه ... بعد از اینکار این فایل دیگه نباید با نرم افزار های نمایش عکس باز بشه...
# dd if=logo.ppm bs=1 skip=15 of=temp-logo && mv temp-logo logo.ppm
442368+0 records in
442368+0 records out
442368 bytes (442 kB, 432 KiB) copied, 1.22259 s, 362 kB/s
و در نهایت با دستور openssl فایل logo.ppm رو رمز میکنیم.. یادمون باشه که فایل logo.ppm الان فقط شامل دیتای RGB هست..
# cat logo.ppm | openssl enc -aes-128-ecb -K 6162636465666768696a6b6c6d6e6f70 -nosalt -out enc-rgb.data
# cat enc-rgb.data >> header.bin
# mv header.bin aes-ecb-logo.ppm
توی دستورات بالا دیتای RGB رو رمز کردیم و cipher-text به دست اومده رو به header.bin اضافه کردیم... و نام header.bin رو به aes-ecb-logo.ppm تغییر دادیم..
تصویری که به دست اومد به این شکله...
و این عکس الان مصداق بارز الگوریتم رمزنگاری ضعیف هست... مفهوم تصویر کاملا مشخصه فقط شاید از لحاظ جزئیات یکم تغییر کرده باشه...
حالا عکس اورجینال رو با استفاده از rabbit و با زبان گولنگ encrypt میکنیم.. تا ببینیم چه تفاوتی داره..
package main
import (
"io"
"os"
"log"
"snix.ir/rabbitio"
)
const (
headerLen = 15
)
var (
key = []byte("abcd1234abcd1234") // key 16 byte
ivx = []byte("random-x") // iv 8 byte
)
func main() {
log.SetFlags(0) // just logs, no time and date...
// open logo-orginal.ppm file...
org, err := os.Open("logo-orginal.ppm")
if err != nil {
log.Fatal(err)
}
// create a file to write cipher-text in it.
enc, err := os.Create("logo-rabbit.ppm")
if err != nil {
log.Fatal(err)
}
// write header to logo-rabbit.ppm without encryption...
hnn, err := io.CopyN(enc, org, headerLen)
if err != nil {
log.Fatal(err)
}
log.Printf("ppm header to file: %d byte copied", hnn)
// new rabbit writer cipher ..
stream, err := rabbitio.NewWriterCipher(key, ivx, enc)
if err != nil {
log.Fatal(err)
}
// copy data through rabbit cipher..
if _, err := io.Copy(stream, org); err != nil {
log.Fatal(err)
}
log.Printf("done *** ")
}
با استفاده از کامپایلر go کد زیر رو کامپایل و اجرا میکنیم...
# nano main.go
# go mod init rabpic && go mod tidy
# go build -o rabbit
# ./rabbit
ppm header to file: 15 byte copied
done ***
و در نهایت تصویر به دست اومده به این شکل هست..
خب هیچ چیزی از تصویر اصلی مشخص نیست.. و تصویر کاملا encrypt شده.. برای decrypt کردن کافیه برنامه رو یکبار دیگه اجرا کنید.. البته برای عکس ورودی باید همین تصویری که رمز شده رو بدید ... اگه اینکارو انجام بدید خروجی برنامه تصویر اصلی میشه ...
بررسی پرفورمنس rabbit: قبل از هر چیز پروژه eSTREAM یک بنچمارکی تهیه کرده که توش بیشتر cipher ها رو از نظر پرفورمنس با هم مقایسه کرده.. بنچمارک پردازنده pentium m
اگه به لیست دقت کنید متوجه میشید که rabbit بین 10 الگوریتم برتر هست.. البته بیشتر این الگوریتم ها به دلیل اسیب پذیری هایی که داشتن نتونستن فاز های مربوطه eSTREAM رو با موفقیت بگذرونن و شکست خوردن. توی این لیست فقط HC-128 و HC-256 موقعیت برتری نسبت به rabbit دارن. البته الگوریتم های HC-128 و HC-256 یک مشکل پرفورمنسی دارن.. شاید از نظر stream کردن یه مقدار از rabbit سریع تر باشن اما فانکشن های setup iv این الگوریتم ها به شدت کُند هست.. پس برای رمزنگاری cipher-text های کوتاه اصلا مناسب نیستن..
تا حالا که تا اینجای کار اومدیم بریم برای تست benchmark این cipher در گولنگ ... کد تست مربوطه به این صورت نوشته شده.. برای اینکه بتونیم مقدار زمان و منابعی که برای این الگوریتم صرف میشه رو محاسبه کنیم من دوتا تست اوکی میکنم، یک تست که فقط یک مقدار معینی از داده رو از یک بافر به بافر دیگه کپی میکنه و یک تست دیگه که علاوه بر کپی دیتا رو با rabbit cipher رمز هم میکنه.. در نهایت میتونیم با کم کردن زمان و منابع تست کُپی از تست rabbit مقدار تقریبی زمان و منابع رو مشخص کنیم..
package rabbit_test
import (
"bytes"
"io"
"math/rand"
"strings"
"testing"
"snix.ir/rabbitio"
)
const dataLEN = 2 << 10 // 2 KB
var TEXT = makeRandomText(dataLEN)
func makeRandomText(n int) string {
b := make([]byte, n)
chr := []byte("JDDYDG")
for i := range b {
b[i] = chr[rand.Intn(len(chr))]
}
return string(b)
}
func BenchmarkRabbit(b *testing.B) {
key := []byte("abcd1234abcd1234") // key 16 byte
ivx := []byte("random-x") // iv 8 byte
msg := strings.NewReader(TEXT)
bf := new(bytes.Buffer)
cph, _ := rabbitio.NewWriterCipher(key, ivx, bf)
if _, err := io.Copy(cph, msg); err != nil {
b.Fatal(err)
}
}
func BenchmarkCopy(b *testing.B) {
msg := strings.NewReader(TEXT)
bf := new(bytes.Buffer)
_, err := io.Copy(bf, msg)
if err != nil {
b.Fatal(err)
}
}
و این نتیجه تست روی سیستم من هست.. توی کد بالا 2 کیلوبایت رشته رو کپی و رمز کردیم... نتایج تست روی سیستم های مختلف ممکنه متفاوت باشه
# go test -v -bench=. -benchmem -count 4
goos: linux
goarch: amd64
pkg: rabpic
cpu: Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz
BenchmarkRabbit
BenchmarkRabbit-8 1000000000 0.0000122 ns/op 0 B/op 0 allocs/op
BenchmarkRabbit-8 1000000000 0.0000140 ns/op 0 B/op 0 allocs/op
BenchmarkRabbit-8 1000000000 0.0000124 ns/op 0 B/op 0 allocs/op
BenchmarkRabbit-8 1000000000 0.0000141 ns/op 0 B/op 0 allocs/op
BenchmarkCopy
BenchmarkCopy-8 1000000000 0.0000021 ns/op 0 B/op 0 allocs/op
BenchmarkCopy-8 1000000000 0.0000026 ns/op 0 B/op 0 allocs/op
BenchmarkCopy-8 1000000000 0.0000019 ns/op 0 B/op 0 allocs/op
BenchmarkCopy-8 1000000000 0.0000027 ns/op 0 B/op 0 allocs/op
PASS
ok rabpic 0.014s
درنهایت امیدوارم از این پست لذت برده باشید.. کامنت و نظر فراموش نشه!
این مطلب از وبلاگ توسعه دهندگان SNIX کپی برداری شده است... لینک پست درblog.snix.ir
مطلبی دیگر از این انتشارات
عملیات CRUD بر روی بانک mysql در زبان go
مطلبی دیگر از این انتشارات
کتابخانه های استاندارد در Go (بخش اول)
مطلبی دیگر از این انتشارات
چرا خوبه که Golang رو یاد بگیریم؟