وبلاگ

رمزنگاری

رمزنگاری

3 دی , 1396

در این مقاله، فرمت رشته ای باینری را برای پیام های پروتکل بافر توصیف خواهیم کرد. برای استفاده از پروتکل بافرها در اپلیکیشن خود نیازی نیست به طور کامل از محتوای مقاله مطلع شوید، زیرا این مقاله جهت پی بردن به نحوه ی تأثیرگذاری فرمت پروتکل بافرهای مختلف بر اندازه ی پیام های رمزگذاری شده ی شما بسیار کارآمد است.

یک پیام ساده

فرض کنیم شما پیام بسیار ساده ی زیر را تعریف می کنید:

message Test1 {
required int32 a = 1;
}

در یک اپلیکیشن،  پیام Test1 را ایجاد کرده و طول آن را از a تا 150 قرار می دهید. سپس، پیام را در قالب یک رشته ی خروجی سریال سازی می کنید. اگر بتوانید پیام رمزگذاری شده را بررسی کنید، سه بایت مشاهده خواهید کرد:

08  96  01

به نظر بسیار کوچک و عددی است. اما معنای این اعداد چیست؟

متغیرهای پایه ی 128

برای پی بردن به فرآیند رمزگذاری پروتکل بافر، بایستی ابتدا با varints یا متغیرها آشنا شوید. متغیرها، روشی برای مرتب سازی اعداد صحیح با استفاده از یک یا چند بایت هستند. اعداد کوچک تر، تعداد بایت های کم تری دارند.

در یک متغیر، هر بایت به جز بایت آخر، مهم ترین بیتی است که در عدد باینری بالاترین ارزش را دارد (msb).  این امر نشان می دهد که بایت های بعدی نیز وارد مجموعه خواهند شد. 7 بیت پایین تر هر بایت برای ذخیره سازی دو عنصر مکمل از اعداد در گروه های 7 بیتی استفاده می شود که در آغاز کم ترین ارزش را داشتند.

برای نمونه، در زیر، عدد 1 نشان داده شده که تنها از یک بایت مفرد تشکیل شده است. بنابراین msb نمی تواند یک مجموعه باشد:

0000 0001

عدد بعدی ،300 است. این عدد کمی پیچیده تر است:

1010 1100 0000 0010

چگونه می توانید نشان دهید که این عدد 300 است؟ ابتدا باید msb را از هر بایت بیرون کشید. زیرا تنها با این روش می توان فهمید که آیا به نقطه ی انتهایی یک عدد رسیده اید یا خیر (همان طور که می بینید، عدد مورد نظر شما در اولین بایت گنجانده شده، زیرا بیش از یک بایت در هر متغیر موجود می باشد).

1010 1100 0000 0010

→ 010 1100  000 0010

شما دو گروه هفت بیتی را وارونه کردید، زیرا همان طور که به خاطر دارید، متغیرها، ابتدا اعداد دارای کم ارزش ترین بیت را ذخیره می کنند. سپس برای رسیدن به مقدار پایانی آنها را وارد می کنید:

000 0010  010 1100

→  000 0010 ++ 010 1100

→  100101100

300= 256 + 32 + 8 + 4 →

ساختار پیام

همان طور که می دانید، یک پیام پروتکل بافر مجموعه ای از جفت مقادیر کلیدی است. نسخه ی باینری یک پیام تنها از فیلدهایی همچون name استفاده می کند و نوع فیلد (type)، تنها با رمزگشایی و یا تعریف نوع پیام آشکار می شود (یعنی proto file).

زمانی که یک پیام رمزگذاری می شود، همه ی کلیدها و مقادیر در قالب بایت و به صورت سریالی سازماندهی می شوند. وقتی پیامی رمزگشایی می شود، تجزیه گرها بایستی بتوانند فیلدهای شناسایی نشده را حذف کنند. به این ترتیب، فیلدهای جدید بدون حذف برنامه های قدیمی که هیچ اطلاعاتی در خصوص آنها موجود نبوده، به پیام افزوده می شوند. به این منظور، مقدار کلیدی هر یک از جفت فیلدها در پیام هایی با فرمت رشته ای در نهایت دارای دو  مقدار خواهد بود؛ یکی عدد فیلد از فایل proto و دیگری نوع رشته که اطلاعات لازم برای یافتن مقادیر زیر را نمایان می کنند.

انواع رشته های موجود در این مبحث به صورت زیر است:

موارد کاربرد مفهوم انواع
int32, int64, uint32, uint64, sint32, sint64, bool, enum Varint 0
fixed64, sfixed64, double 64-bit 1
string, bytes, embedded messages, packed repeated fields Length-delimited 2
groups (deprecated) Start group 3
groups (deprecated) End group 4
fixed32, sfixed32, float 32-bit 5

هر مقدار کلیدی در پیام،  یک متغیر دارای ارزش است (field-number≤3 |wire-type). به بیانی دیگر، سه بیت پایانی عدد نوع رشته را ذخیره می کند.

اکنون، دوباره نگاهی به همان مثال ساده ی خود می اندازیم. حالا شما می دانید که اولین عدد در یک رشته همواره متغیری کلیدی است و در این مثال منظور ما همان 08 است یا (با بیرون کشیدن msb):

000  1000

شما بایستی سه بیت آخر را داشته باشید تا به نوع 0 برسید. پس از آن، با کنار کشیدن آنها به سمت راست، به فیلد شماره ی 1 می رسید. بنابراین اکنون شما می دانید که “تگ” همان 1 و مقدار پس از آن همان “متغیر” است. با استفاده از دانش رمزگشایی خود از بخش پیش، خواهید دید دو بایت بعدی مقدار 150 را ذخیره می کند.

96 01 = 1001 0110  0000 0001
( msb را بیرون بکشید و گروه های 7 بیتی را برگردانید )       → 000 0001  ++  001 0110
→ 10010110
→ 2 + 4 + 16 + 128 = 150

انواع مقادیر بیشتر

اعداد صحیح علامت دار

همان گونه که در بخش پیش دیدید، تمامی انواع پروتکل بافرهای نوع 0، به عنوان متغیر رمزگذاری می شوند. با این وجود، زمانی که اعداد منفی رمزگذاری می شوند، تفاوت عمده ای میان انواع int های علامت دار (sint32 و sint64) و انواع int های “استاندارد” (int32 و int64) وجود دارد. اگر از int32 و یا int64 برای اعداد منفی استفاده کنید، متغیر حاصل همواره ده بایت طولانی تر خواهد شد- و به شکلی مؤثر عدد صحیح بدون علامت و بسیار بزرگی تلقی می شود.  چنانچه یکی از انواع علامت دار را به کار ببرید، متغیر حاصل از رمزگذاری ZigZag استفاده می کند که بسیار مؤثرتر است.

رمزگذاری ZigZag اعداد صحیح علامت دار را بدون علامت نشان می دهد. بنابراین اعدادی که ارزش های مطلق کم تری دارند (مثلاً 1)، مقادیر رمزگذاری شده ی کمتری نیز خواهند داشت. در این روشِ رمزگذاری، اعداد صحیح به عقب و جلو کشیده و مثلاً 1- به صورت 1، 1 به صورت 2، 2- به صورت 3 و … رمزگذاری می شوند. همان طور که در جدول زیر می بینید:

اعداد رمزگذاری شده اعداد اصلی علامت دار
0  0
1 -1
2  1
3 -2
4294967294  2147483647
4294967295 -2147483647

به عبارت دیگر، هر مقدار n به روش زیر رمزگذاری می شود:

(n << 1) ^ (n >> 31)

برای sint های 32، و یا

(n << 1) ^ (n >> 63)

برای نسخه ی 64 بیتی

در نظر داشته باشید که بخش تغییر یافته ی دوم یعنی بخش (n >> 31)، یک تغییر مکان محاسباتی است. بنابراین نتیجه ی این تغییر مکان یا عددی است با تمام بیت های صفر  (چنانچه n مثبت باشد) و یا عددی است با تمام بیت های 1 (چنانچه n منفی باشد).

زمانی که sint32 یا sint64 تجزیه می شوند، مقدار آنها به صورت همان نسخه ی علامت دار اصلی رمزگشایی می شود.

اعداد نامتغیر

اعداد نامتغیر ساده اند؛ مانند انواع double و fixed64 که رشته ی نوع 1 دارند و به تجزیه گر اطلاع می دهد توده ای از داده های ثابت 64 بیتی در نظر داشته باشد. به همبن ترتیب، انواع float و fixed32 دارای رشته های نوع 5 بوده و نشان می دهند که دارای توده داده های 32 بیتی هستند. این مقادیر، در هر دو مورد، به ترتیب بایت های کم ارزش ذخیره می شوند.

رشته ها

رشته ای از نوع 2 (با طول مشخص) یعنی این رشته مقدار متغیری است با طول رمزگذاری شده که تعداد مشخصی از  بایت ها در داده ها پس از آن قرار می گیرند:

message Test2 {
required string b = 2;
}

 

با جای گذاری مقدار b، به اعداد زیر خواهیم رسید:

12 07 74 65 73 74 69 6e 67

بایت های قرمز همان UTF8 هستند. در اینجا، کلید Key = 0×12 ،tag=2 و type=2 می باشد. طول متغیر 7 است و اینک 7 بایت پس از آن وجود دارد- یعنی رشته ی ما.

پیام های جاسازی شده

در اینجا با ارائه ی مثالی به تعریف یک پیام جاسازی شده در Test1 می پردازیم:

message Test3 {
required Test1 c = 3;
}

و در اینجا نسخه ی رمزگذاری شده ی آن را باز هم با استفاده از Test1، فیلدی که تا 150 جای گذاری شده، ارائه می دهیم:

1a 03 08 96 01

همان طور که می بینید، سه بایت پایانی دقیقاً مشابه همان مثال اول بوده (08 96 01) و پس از عدد 3 قرار گرفته اند- یعنی با پیام های جاسازی شده دقیقاً مانند رشته ها رفتار می شود ( wire type = 2).

عناصر اختیاری و تکراری

چنانچه پیامی از نوع proto2 عناصر تکراری داشته باشد (بدون عنصر [packed=true])، پیام رمزگذاری شده، یا هیچ جفت مقادیر کلیدی ندارد و یا بیش از چند جفت مقادیر کلیدی با تگ مشابه خواهد داشت. این مقادیر تکراری نباید به طور متوالی ظاهر شوند، بلکه ممکن است با سایر فیلدها جای گذاری شوند. در proto3، فیلدهای تکراری از رمزگذاری بسته بندی شده استفاده می کنند، که به این منظور می توانید مطالب زیر را مطالعه نمایید.

برای هر فیلد غیرتکراری در proto3 یا فیلدهای اختیاری در proto2، پیام رمزگذاری شده می تواند یک جفت مقادیر کلیدی با همان عدد تگ داشته و یا فاقد آن باشد.

به طور معمول، یک پیام رمزگذاری شده هرگز بیش از یک فیلد غیرتکراری ندارد. با این وجود، انتظار می رود تجزیه گرها به مواردی بپردازند که آن را انجام می دهند. برای انواع عددی و رشته ها، چنانچه یک فیلد مشابهی چندین مرتبه ظاهر شود، تجزیه گر آخرین مقدار را در نظر می گیرد. در مورد فیلدهای یک پیام جاسازی شده، تجزیه گر چند نمونه از یک رشته ی مشابه را ادغام می کند، گویی یک پیام را ادغام می کند: روش MergeForm- به این مفهوم که همه ی فیلدهای عددی تکی در نمونه ی دوم، آنها را در نمونه ی اول جایگزین می کند، پیام های جاسازی شده ی تکی ادغام و فیلدهای تکراری مرتب می شوند. تأثیر این قوانین بدین گونه است که تجزیه ی دو پیام جاسازی شده ی الحاقی، دقیقاً نتایجی مشابه تجزیه ی پیام های جداگانه و ادغام عناصر حاصل دارد. به این معنی که پیامِ:

MyMessage message;
message.ParseFromString(str1 + str2);

مشابه فرمت زیر است:

MyMessage message, message2;
message.ParseFOrmString(str1);
message2.ParseFormString(str2);
message.MergeForm(message2);

این روش بسیار کاربردی است، زیرا این امکان را به شما می دهد تا حتی بدون اطلاع از نوع پیام ها، بتوانید دو پیام را با هم ادغام کنید.

فیلدهای تکراری بسته بندی شده

نسخه ی 2.1.0 فیلدهای تکراری بسته بندی شده را معرفی می کند که در proto2، همانند فیلدهای تکراری اما با گزینه ی [packed=true] مشخص می شوند. در proto3، فیلدهای تکراری به صورت پیش فرض بسته بندی می شوند. درست است این فیلدها همانند فیلدهای تکراری هستند اما به روش متفاوتی رمزگذاری می شوند. یک فیلد تکراری بسته بندی شده شامل عناصر صفر است که در پیام رمزگذاری شده، نمایان نمی شود. در غیر این صورت، همه ی عناصر فیلد به صورت یک جفت مقدار کلیدی دارای رشته ی نوع 2 (با طول مشخص) بسته بندی می شوند. هر عنصر به همان صورت معمولی رمزگذاری می گردد، با این اختلاف که هیچ تگی پیش از آن قرار نمی گیرد.

برای نمونه، فرض کنید پیامی به صورت زیر دارید:

message Test4 {
repeated int32 d = 4 [packed=true];
}

حالا ضمن داشتن مقادیر 3، 270 و 86942 برای فیلد تکراری d، می توانید یک Test4 بسازید. سپس، فرمت رمزگذاری شما بدین صورت خواهد بود:

22       // tag (field number 4, wire type 2)

06       // payload size (6 bytes)

03       // first element (varint 3)

8E 02     // second element (varint 270)

9E A7 05 // third element (varint 86942)

تنها فیلدهای تکراری از نوع ارقام ابتدایی (فیلدهایی از نوع 32 بیت و 64 بیت) را می توان به صورت بسته بندی شده در نظر گرفت.

در نظر داشته باشید با وجود این که معمولاً دلیلی برای رمزگذاری بیش از یک جفت مقدار کلیدی برای یک فیلد تکراری وجود ندارد، رمزگذاران بایستی بتوانند جفت مقادیر کلیدی چندگانه را بپذیرند. در این صورت، عناصر حامل می بایست مرتب شوند و هر جفت باید شامل کل عناصر باشد.

تجزیه گرهای پروتکل بافر باید قادر به تجزیه ی آن دسته از فیلدهای تکراری باشند که به صورت بسته بندی شده وارد می شوند، به گونه ای که گویی بسته بندی نشده اند، و برعکس. این امر منجر به افزودن [packed=true] به فیلدهای موجود به روش عقب و جلو خواهد شد.

ترتیب فیلد

با وجود این که می توان از فیلدهای یک proto با هر ترتیبی استفاده کرد، زمانی که پیامی سریال سازی می شود، فیلدهای شناخته شده ی آن بایستی با توجه به شماره ی فیلدها مرتب شوند؛ مثلاً در سریال سازی برنامه ی C++ ، Java و Python. بنابراین، با تکیه بر شماره ی فیلد که پی در پی قرار گرفته است، امکان کاربرد بهینه را برای کد تجزیه شده  فراهم می سازد. با این وجود، تجزیه گرهای پروتکل بافر باید قادر به تجزیه ی فیلدها با هر ترتیبی باشند زیرا تمام پیام ها با سریال سازی ساده ی یک آبجکت ایجاد نمی شوند؛ برای نمونه، گاهی اوقات ادغام دو پیام به روش مرتب کردن ساده ی آنها کاربردی تر است.

چنانچه پیامی فیلدهای ناشناخته دارد، مانند اجرای برنامه های C++ و  Java، آنها را به روش دلخواه پس از فیلدهای شناخته و مرتب شده قرار دهید. اجرای برنامه ی Python فعلی فیلدهای ناشناخته را دنبال نمی کند.

دیدگاه