उदासीनता: पेपर संरक्षण कैसे काम करता है

एडमिट, जो बचपन में "डैंडी" या "सेगा" खेल खेलने में घंटों बिताते थे? और जो आप खेल के माध्यम से प्रगति करते हैं, उसने कागज के एक टुकड़े पर या विशेष रूप से घाव नोटबुक में पासवर्ड लिखा है? यदि यह आप है, और जब से आप इस साइट को पढ़ रहे हैं, तो आप शायद कम से कम एक बार सवाल पूछें: "यह कैसे काम करता है?"

मैं अपने बचपन के खेलों से उदाहरणों का उपयोग करके पासवर्ड बनाने के लिए क्लासिक तंत्र के सिद्धांतों को समझाने की कोशिश करूंगा। मैं इस तथ्य के लिए अग्रिम रूप से माफी मांगता हूं कि सभी उदाहरण एनईएस प्लेटफॉर्म (हां, "डैंडी") से होंगे, हालांकि विषय वस्तु इसके लिए सीमित नहीं है। यह सिर्फ इतना हुआ कि मुझे थोड़ा और शोध करने और थोड़ा और पाठ लिखने के लिए अपने आप में पर्याप्त प्रेरणा नहीं मिली।

मैं सरल से जटिल तक के क्रम में उदाहरण दूंगा। सबसे पहले, थोड़ा कोड होगा, लेकिन मानव भाषा में एल्गोरिदम को समझाने के लिए जितना अधिक कठिन होगा और तकनीकी भाषा में उन्हें समझाना उतना आसान होगा, इसलिए मुझे दोष न दें।



कोड बुक


मेरे पास अभी भी मेरी यादों में मेरे बचपन के खेल के कुछ पासवर्ड हैं - उदाहरण के लिए, "क्रेजीलैंड में ट्रॉल्स" ("डोकी! डोकी! यूनुची") से "BCHK", "4660" "चौगिन जय - जेटमैन"। हम कह सकते हैं कि ये ऐसे पासवर्ड हैं जो सुविधा के लिहाज से आदर्श हैं: इन्हें याद रखना आसान है और प्रवेश करते समय गलतियाँ करना मुश्किल है। लेकिन उनमें कितनी जानकारी हो सकती है, और इस तरह के पासवर्ड को बेतरतीब ढंग से लेने का मौका क्या है?

पहले मामले में, पासवर्ड वर्णमाला 24 वर्ण है। यदि आप वर्णों के संयोजन की संख्या की गणना करते हैं, तो यह 24 4 होगा - इतना छोटा नहीं है, यह देखते हुए कि खेल में केवल 12 स्तर हैं, और वास्तव में, पासवर्ड स्तर संख्या के अलावा कुछ भी संग्रहीत नहीं करता है। हम कई गुप्त पासवर्डों को ध्यान में रखेंगे और एक कोशिश पर एक पासवर्ड लेने की संभावना की गणना करेंगे: (12 + 4) / 24 4 , जो ~ 5.7 × 10 -14 के बराबर है। दूसरे शब्दों में, किसी भी वास्तविक को चुनने से पहले आपको 17592186044416 पासवर्ड का औसत आज़माना होगा।

दूसरे मामले में, सब कुछ कुछ अलग है। जाहिर है, 4 अंकों का एक सेट हमें बिल्कुल 10,000 (10 4 ) संयोजन देता है। खेल में पाँच स्तर होते हैं जिन्हें एक अलग क्रम में पूरा किया जा सकता है और कठिनाई के दो स्तर। यानी पासवर्ड गुजरे स्तरों और कठिनाई स्तर के बारे में जानकारी संग्रहीत करता है। इस प्रकार, मौजूदा पासवर्ड की संख्या 2 × 2 5 है , अर्थात। 64. इसलिए पासवर्ड चुनने की संभावना 0.0064 है, अर्थात आधा प्रतिशत से अधिक। क्या यह पर्याप्त नहीं है? औसतन, लगभग हर 156 वां पासवर्ड सही होगा, और खोज की उच्च गति को देखते हुए, खोज लंबे समय तक नहीं चलेगी। और, स्पष्ट रूप से, बचपन में, हम अक्सर खेल को "क्रूर बल" देते हैं जब हम शुरू से ही शुरू नहीं करना चाहते थे।

वास्तव में, ऐसे पासवर्ड की सूचना क्षमता का मूल्यांकन करने के लिए अर्थहीन है, क्योंकि वे केवल एक प्रकार की कुंजी हैं, अर्थात्। गेम केवल सभी संभावित पासवर्डों को संग्रहीत करते हैं, और दर्ज किए गए पासवर्ड के सूचकांक से उन्हें स्तर और इसी तरह की जानकारी मिलती है। लेकिन हित के लिए मैं कहूंगा कि उनकी सैद्धांतिक क्षमता 48 और 13 बिट्स (लॉग 2 24 4 और लॉग 2 10 4 ) है।

और फिर भी, प्रवेश किए गए पासवर्ड को वास्तव में कैसे नियंत्रित किया जाता है? पहले मामले में, दर्ज किया गया पासवर्ड किसी भी रूपांतरण से नहीं गुजरता है, लेकिन केवल पासवर्ड के संग्रहित सरणी में देखा जाता है।
कोड दिखाएं
const char* s_passwords[12] = { " ", "BLNK", // ... "MZSX" }; // ... if (strncmp(pass, s_secretPassword1, 4) == 0) { callSecret1(); return 0; } // ... for (int level = 0; level < 12; level++) { if (strncmp(pass, s_passwords[level], 4) == 0) { return level; } } return -1; 


दूसरे मामले में, खेल में कुछ होशियार आता है, और पहले में पासवर्ड परिवर्तित करने के लिए किए बीसीडी कोड है, जो अपने आकार वास्तव में दो बार कम कर देता है। इससे गेम में पासवर्ड के आकार को आधे से कम करना संभव हो जाता है।
कोड दिखाएं
  uint16 toBCD(const char* pass) { uint16 result = 0; for (int i = 0; i < 4; i++) { result <<= 4; result |= (pass[i] - '0') & 0xF; } return result; } s_passwords[2][32] = { { 0x0000, // ... 0x4660 }, { 0x7899, // ... 0x5705 } }; // ... const uint16 pass = toBCD(passStr); for (int difficulty = 0; difficulty < 2; difficulty++) { for (int clearedLevels = 0; clearedLevels <= 0x1F; clearedLevels++) { if (pass == s_passwords[difficulty][clearedLevels]) { setState(difficulty, clearedLevels); return true; } } } return false; 


संख्या और संख्या


मैं या तो क्लासिक्स को नजरअंदाज करने की हिम्मत नहीं करता: मुझे यकीन है कि कई लोगों ने मूल "प्रिंस ऑफ पर्शिया" खेला है, न कि केवल कैंडी। गेम पासवर्ड भी दशमलव अंकों के अनुक्रम होते हैं, लेकिन इस बार वे कुछ डेटा को एनकोड करते हैं।

अर्थात्, दो मान एनकोड किए गए हैं: समय और स्तर संख्या। क्योंकि पासवर्ड 8 अंक लंबा है, अर्थात 100 मिलियन संयोजन है, लेकिन इस खेल में 14 के स्तर और समय (840 विकल्पों में से एक कुल) के 60 संभावित मान, यह माना जा सकता है कि यह पता लगाने के लिए मुश्किल है। लेकिन वास्तव में ऐसा नहीं है, और यह समझने के लिए कि, चलो पहले इसकी पीढ़ी के सिद्धांत की जांच करें।

तो, पहले खेल 8 तत्वों की एक सरणी बनाता है जो मानों को 0 से 9 तक संग्रहीत कर सकते हैं। फिर दो यादृच्छिक मान उत्पन्न होते हैं - यह भी 0 से 9 तक - और सूचक 2 और 5 पर इस सरणी के लिए लिखे गए हैं। ये यादृच्छिक मान वेतन वृद्धि हैं, जो करने के लिए संग्रहीत मूल्यों 10. यह बढ़ जाती है संभव पासवर्ड की संख्या 100 गुना है, जो स्पष्ट रूप से पैटर्न की पहचान पेचीदा सापेक्ष जोड़ दिया जाएगा।
  const uint8 rand0 = rand() % 10; const uint8 rand1 = rand() % 10; char pass[8] = {0, 0, rand0, 0, 0, rand1, 0, 0}; 

अगला, स्तर सूचकांक एन्कोडेड है (यानी इसकी संख्या शून्य से एक): सूचकांक के दो सबसे महत्वपूर्ण बिट्स का योग और दूसरा इंक्रीमेंट मॉडुलो 10 इंडेक्स 7 पर लिखा गया है, और दो कम से कम महत्वपूर्ण बिट्स का योग और पहला इंक्रीमेंट इंडेक्स 1 पर लिखा गया है।
  //       pass[7] = ((level >> 2) + rand1) % 10; //     pass[1] = ((level & 3) + rand0) % 10; 

समय की बारी आ गई है। यह थोड़ा आसान है: दसियों का योग और पहला वेतन वृद्धि modulo 10 सूचकांक 0 पर लिखा है, और इकाइयों का योग और सूचकांक में दूसरी वृद्धि 3। यह पता चला है कि शून्य के बराबर वेतन वृद्धि के साथ, समय दशमलव अंकों में लिखा जाता है, जैसा कि यह है। और, चूंकि दर्जनों की छत 9 है, अधिकतम संभव समय मान 99 है, और 60 मिनट "ईमानदार" नहीं है।
  //     pass[0] = ((time / 10) + rand0) % 10; //     pass[3] = ((time % 10) + rand1) % 10; 

डेटा दर्ज किया गया है, यह पासवर्ड की वैधता को सत्यापित करने के लिए चेकसम की गणना करने के लिए बना हुआ है।
  //    sum = pass[0] + pass[1] + pass[2] + pass[3]; sum += (sum % 10) + pass[5]; sum += (sum / 10) + pass[7]; //    pass[4] = sum % 10; pass[6] = sum / 10; 

उदाहरण के लिए, 32 वें शेष मिनट के साथ 13 वें स्तर के पासवर्ड की किंवदंती "96635134" है।


यह स्पष्ट हो जाता है कि पासवर्ड चयन के लिए यह पर्याप्त है कि चेकसम एन्कोडेड डेटा के लिए उपयुक्त है। फिर, यदि आप इसकी बारीकियों को ध्यान में नहीं रखते हैं, तो आप पासवर्ड चुनने की संभावना की गणना कर सकते हैं: ठीक 1% (राशि के संभावित मूल्यों की संख्या से विभाजित इकाई) - काफी।

लेकिन यह सिर्फ एक सामान्य राशि है! और विभिन्न डेटा के लिए, यह एक ही हो सकता है। यदि आप कोई वैध पासवर्ड बदलते हैं ताकि पहले 4 अंकों का योग समान रहे - तो यह होगा। हम क्या कह सकते हैं कि एक साधारण राशि का वितरण एक समान नहीं है, और ऐसी राशि का अधिकतम मूल्य कभी 72 से अधिक नहीं होगा।

इतनी बड़ी राशि की बारीकियों को देखते हुए ऐसा लगता है कि जब बार-बार दोहराना एक रेखीय संभावना यह बहुत बड़े डेटा मेल खाता है। यही कारण है कि नेटवर्क पर विषयगत मंचों पर आप अक्सर लोगों की यादों को पूरा कर सकते हैं कि उन्होंने कितने अच्छे तरीके से राजकुमार फारस के पासवर्ड चुने।

स्थिति संख्या प्रणाली


बेस 64 और बेस 32 से निश्चित रूप से परिचित हैं। उनकी परिभाषा के अनुसार, ये क्रमशः 64 और 32 ठिकानों के साथ स्थितीय संख्या प्रणाली हैं। सिद्धांत सरल है: एक निश्चित बिट लंबाई मूल्य पर बिटस्ट्रीम विभाजित है, और फिर अनुक्रमित के रूप में प्राप्त मानों के एक विशिष्ट प्रतीकों शब्दकोश ले।

कई पासवर्ड सिस्टम इस सिद्धांत पर आधारित हैं। और अगला गेम, जिसके उदाहरण पर मैं आपको पासवर्ड जनरेशन एल्गोरिदम बताऊंगा, Takahashi Meijin no Bouken Jima IV होगा, जिसे आम लोगों में एडवेंचर आइलैंड 4 के रूप में जाना जाता है।

खेल की स्थिति में उपलब्ध वस्तुओं (12 तक), क्षमताओं (3 तक), विशेष आइटम (6 तक), एकत्रित दिल (8 तक), अंडे के साथ स्थान और पारित स्तरों के बारे में जानकारी का एक सेट शामिल है। लेकिन वास्तव में, दिल और विशेष वस्तुओं को छोड़कर सब कुछ के लिए, एक मूल्य जिम्मेदार है - प्रगति का सूचकांक। यह एक सरणी में एक सूचकांक है जहां प्रत्येक तत्व उपलब्ध वस्तुओं, क्षमताओं और अधिक के बारे में जानकारी संग्रहीत करता है। सीधे शब्दों में कहें, यह यह बाइट है जो वस्तुओं के सेट, क्षमताओं और निर्धारित चरणों को परिभाषित करता है।

एल्गोरिथ्म का पहला चरण चार बाइट्स की एक सरणी का निर्माण करना है। प्रगति का मूल्य पहली बाइट में लिखा गया है। दिलचस्प है, केवल कुछ मूल्यों की अनुमति है।

दूसरे बाइट में, उपलब्ध विशेष वस्तुओं का मुखौटा दर्ज किया गया है - बाइट के 6 सबसे महत्वपूर्ण बिट्स। शेष निचले 2 बिट्स में, एक के बराबर एक स्थिर लिखा जाता है। मुझे लगता है कि यह पासवर्ड प्रारूप संस्करण है। और शायद दर्ज किए गए पासवर्ड का केवल एक निरंतर अधिक कठोर सत्यापन।

स्थान सूचकांक जहां अंडे रखा गया है, तीसरे बाइट में लिखा जाता है (उन लोगों के लिए जो नहीं खेलते थे, एक प्रकार की चौकी)। यदि अंडा कहीं भी स्थापित नहीं है, तो मान 0xFF है। एक अंडे के साथ स्थान सूचकांक भी केवल कुछ मान ले सकता है - इसमें केवल उन स्थानों को शामिल किया जाता है जहां अंडा स्थापित किया जा सकता है।

और अंत में, एकत्रित दिलों और आधे दिलों के मुखौटे को चौथे बाइट में कॉपी किया जाता है।

टेबल दिखाओ
  //        const uint8 s_itemsInProgress[] = { 0x8000, 0xC000, 0xC000, 0xC000, 0xE000, 0xF000, 0xF000, 0xF000, 0xF000, 0xF800, 0xFC00, 0xFC00, 0xFC00, 0xFC00, 0xFE00, 0xFF00, 0xFF00, 0xFF00, 0xFF00, 0xFF00, 0xFF80, 0xFF80, 0xFF80, 0xFFC0, 0xFFC0, 0xFFE0, 0xFFE0, 0xFFF0, 0xFFF0, 0xFFF0, 0xFFFC, 0xFFFE, 0xFFFF, 0xFFFF }; //        const uint8 s_powersInProgress[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0 }; //     const uint8 s_accessibleEggLocations[] = { 0x04, 0x07, 0x16, 0x1B, 0x2F, 0x31, 0x41, 0x43, 0x45, 0x47, 0x4E, 0x52, 0x57, 0x87, 0x98, 0x9C, 0x9E, 0xA0, 0xA1, 0xA2, 0xA4, 0xB1, 0xB3, 0xB5, 0xFF, 0x0C }; //    const uint8 s_accessibleProgressValues[] = { 0, 1, 4, 5, 6, 9, 10, 11, 14, 15, 16, 17, 20, 21, 22, 23, 26, 27, 28, 29 }; 0xE000, 0xF000, 0xF000, 0xF000,  //        const uint8 s_itemsInProgress[] = { 0x8000, 0xC000, 0xC000, 0xC000, 0xE000, 0xF000, 0xF000, 0xF000, 0xF000, 0xF800, 0xFC00, 0xFC00, 0xFC00, 0xFC00, 0xFE00, 0xFF00, 0xFF00, 0xFF00, 0xFF00, 0xFF00, 0xFF80, 0xFF80, 0xFF80, 0xFFC0, 0xFFC0, 0xFFE0, 0xFFE0, 0xFFF0, 0xFFF0, 0xFFF0, 0xFFFC, 0xFFFE, 0xFFFF, 0xFFFF }; //        const uint8 s_powersInProgress[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0 }; //     const uint8 s_accessibleEggLocations[] = { 0x04, 0x07, 0x16, 0x1B, 0x2F, 0x31, 0x41, 0x43, 0x45, 0x47, 0x4E, 0x52, 0x57, 0x87, 0x98, 0x9C, 0x9E, 0xA0, 0xA1, 0xA2, 0xA4, 0xB1, 0xB3, 0xB5, 0xFF, 0x0C }; //    const uint8 s_accessibleProgressValues[] = { 0, 1, 4, 5, 6, 9, 10, 11, 14, 15, 16, 17, 20, 21, 22, 23, 26, 27, 28, 29 }; 0xFC00, 0xFC00, 0xFE00, 0xFF00,  //        const uint8 s_itemsInProgress[] = { 0x8000, 0xC000, 0xC000, 0xC000, 0xE000, 0xF000, 0xF000, 0xF000, 0xF000, 0xF800, 0xFC00, 0xFC00, 0xFC00, 0xFC00, 0xFE00, 0xFF00, 0xFF00, 0xFF00, 0xFF00, 0xFF00, 0xFF80, 0xFF80, 0xFF80, 0xFFC0, 0xFFC0, 0xFFE0, 0xFFE0, 0xFFF0, 0xFFF0, 0xFFF0, 0xFFFC, 0xFFFE, 0xFFFF, 0xFFFF }; //        const uint8 s_powersInProgress[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0 }; //     const uint8 s_accessibleEggLocations[] = { 0x04, 0x07, 0x16, 0x1B, 0x2F, 0x31, 0x41, 0x43, 0x45, 0x47, 0x4E, 0x52, 0x57, 0x87, 0x98, 0x9C, 0x9E, 0xA0, 0xA1, 0xA2, 0xA4, 0xB1, 0xB3, 0xB5, 0xFF, 0x0C }; //    const uint8 s_accessibleProgressValues[] = { 0, 1, 4, 5, 6, 9, 10, 11, 14, 15, 16, 17, 20, 21, 22, 23, 26, 27, 28, 29 }; 0xE0, 0xE0, 0xE0, 0xE0,  //        const uint8 s_itemsInProgress[] = { 0x8000, 0xC000, 0xC000, 0xC000, 0xE000, 0xF000, 0xF000, 0xF000, 0xF000, 0xF800, 0xFC00, 0xFC00, 0xFC00, 0xFC00, 0xFE00, 0xFF00, 0xFF00, 0xFF00, 0xFF00, 0xFF00, 0xFF80, 0xFF80, 0xFF80, 0xFFC0, 0xFFC0, 0xFFE0, 0xFFE0, 0xFFF0, 0xFFF0, 0xFFF0, 0xFFFC, 0xFFFE, 0xFFFF, 0xFFFF }; //        const uint8 s_powersInProgress[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0 }; //     const uint8 s_accessibleEggLocations[] = { 0x04, 0x07, 0x16, 0x1B, 0x2F, 0x31, 0x41, 0x43, 0x45, 0x47, 0x4E, 0x52, 0x57, 0x87, 0x98, 0x9C, 0x9E, 0xA0, 0xA1, 0xA2, 0xA4, 0xB1, 0xB3, 0xB5, 0xFF, 0x0C }; //    const uint8 s_accessibleProgressValues[] = { 0, 1, 4, 5, 6, 9, 10, 11, 14, 15, 16, 17, 20, 21, 22, 23, 26, 27, 28, 29 }; 0x2F, 0x31, 0x41, 0x43,  //        const uint8 s_itemsInProgress[] = { 0x8000, 0xC000, 0xC000, 0xC000, 0xE000, 0xF000, 0xF000, 0xF000, 0xF000, 0xF800, 0xFC00, 0xFC00, 0xFC00, 0xFC00, 0xFE00, 0xFF00, 0xFF00, 0xFF00, 0xFF00, 0xFF00, 0xFF80, 0xFF80, 0xFF80, 0xFFC0, 0xFFC0, 0xFFE0, 0xFFE0, 0xFFF0, 0xFFF0, 0xFFF0, 0xFFFC, 0xFFFE, 0xFFFF, 0xFFFF }; //        const uint8 s_powersInProgress[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0 }; //     const uint8 s_accessibleEggLocations[] = { 0x04, 0x07, 0x16, 0x1B, 0x2F, 0x31, 0x41, 0x43, 0x45, 0x47, 0x4E, 0x52, 0x57, 0x87, 0x98, 0x9C, 0x9E, 0xA0, 0xA1, 0xA2, 0xA4, 0xB1, 0xB3, 0xB5, 0xFF, 0x0C }; //    const uint8 s_accessibleProgressValues[] = { 0, 1, 4, 5, 6, 9, 10, 11, 14, 15, 16, 17, 20, 21, 22, 23, 26, 27, 28, 29 }; 0xA4, 0xB1, 0xB3, 0xB5,  //        const uint8 s_itemsInProgress[] = { 0x8000, 0xC000, 0xC000, 0xC000, 0xE000, 0xF000, 0xF000, 0xF000, 0xF000, 0xF800, 0xFC00, 0xFC00, 0xFC00, 0xFC00, 0xFE00, 0xFF00, 0xFF00, 0xFF00, 0xFF00, 0xFF00, 0xFF80, 0xFF80, 0xFF80, 0xFFC0, 0xFFC0, 0xFFE0, 0xFFE0, 0xFFF0, 0xFFF0, 0xFFF0, 0xFFFC, 0xFFFE, 0xFFFF, 0xFFFF }; //        const uint8 s_powersInProgress[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0 }; //     const uint8 s_accessibleEggLocations[] = { 0x04, 0x07, 0x16, 0x1B, 0x2F, 0x31, 0x41, 0x43, 0x45, 0x47, 0x4E, 0x52, 0x57, 0x87, 0x98, 0x9C, 0x9E, 0xA0, 0xA1, 0xA2, 0xA4, 0xB1, 0xB3, 0xB5, 0xFF, 0x0C }; //    const uint8 s_accessibleProgressValues[] = { 0, 1, 4, 5, 6, 9, 10, 11, 14, 15, 16, 17, 20, 21, 22, 23, 26, 27, 28, 29 }; 

  const uint8 s_version = 1; // ... uint8 data[4] = {progress, specItems | s_version, eggLocation, hearts}; 

फिर पासवर्ड को Base32 के समान एन्कोड किया गया है, लेकिन एक अलग वर्णमाला के साथ: इस सरणी से 5 बिट्स को एक-एक करके लिया जाता है और आठ तत्वों की एक सरणी के अलग-अलग बाइट्स में लिखा जाता है। इस मामले में, ऑपरेशन "xor" का उपयोग करना चेकसम है, जो कि सरणी के अंतिम बाइट में लिखा गया है।

छठे बाइट के मुक्त बिट्स में, कोडबुक इंडेक्स जोड़ा जाता है। खेल की शुरुआत में, इस सूचकांक की गणना यादृच्छिक रूप से की जाती है (0 से 3 तक का मूल्य), लेकिन केवल एक ही हमेशा एक मार्ग के भीतर उपयोग किया जाएगा। यानी एक ही पासवर्ड के 4 वेरिएंट हो सकते हैं।

  uint8 password[8] = {}; for (var i = 0; i < 7; i++) { password[i] = takeBits(data, 5); password[7] ^= password[i]; } password[6] |= (tableIndex << 3); password[7] ^= password[6]; 

अंतिम चरण: चार बेस 32 कोडिंग टेबल में से एक को इंडेक्स द्वारा लिया जाता है, और परिणामी सरणी को टेक्स्ट में बदल दिया जाता है। सरणी तत्वों का उपयोग चरित्र सूचकांकों के रूप में किया जाता है।

  const char* s_encodeTables[] = { "3CJV?N4Y0FP78BS1GW2QL6ZM9TR5KDXH", "JT1W9M3DV5?ZKX6GC0FB2SPHR4N8LY7Q", "R0CXM8TWB3G56PKY4FVND7QL2JZ19HS?", "8JWB3PD0?RVG5L2KX4QFZ9TN1S6MH7YC" }; char passwordStr[11] = ""; int index = 0; for (var i = 0; i < 8; i++) { passwordStr[index++] = s_encodeTables[tableIndex][password[i]]; if (i == 3 || i == 5) { passwordStr[index++] = '-'; } } 


32 8 संभावित पासवर्ड विकल्प हैं। उपयुक्त पासवर्डों की संख्या की गणना करना आसान है - बस प्रत्येक एन्कोडेड चर के लिए मान्य मानों की संख्या को गुणा करें। तो, हम स्थानों 26 अंडे, प्रगति के 20 विभिन्न मूल्यों, 256 (2 8) संयोजन एकत्र दिल, 64 (2 6) विशेष आइटम और 4 पासवर्ड विकल्पों का संयुक्त सांकेतिक शब्दों में बदलना कर सकते हैं। कुल: 26 × 20 × 256 × 64 × 4 = 34078720 पासवर्ड। इसलिए, पासवर्ड चुनने की संभावना ~ 0.03% है - हमें औसतन 32,264 प्रयासों की आवश्यकता होगी।

ग्राफिक अराजकता


कुछ मामलों में, डेवलपर्स मूल हैं और ग्राफिक पासवर्ड का उपयोग करते हैं। आप उनका सामना कर सकते हैं, उदाहरण के लिए, मेगा मैन श्रृंखला के खेलों में। बेशक, ऐसे पासवर्ड की उपयोगिता संदिग्ध है - विशेष रूप से एक आदत से। लेकिन यह जापानी में लंबे पासवर्ड की तुलना में कुछ भी नहीं है, जो, अफसोस, मेरे पास इस लेख में कवर करने के लिए पर्याप्त ताकत नहीं थी।

एक उदाहरण के रूप में, मैं गेम पावर ब्लेड 2 को ले जाऊंगा। यह 4x3 ग्रिड में स्थित 12 बोनस आइकन से युक्त पासवर्ड का उपयोग करता है। कुल मिलाकर 8 अलग-अलग आइकन हैं, जिनमें एक खाली है। वास्तव में, इस तरह के एक सांकेतिक पासवर्ड और एक ग्राफिक पासवर्ड के बीच का अंतर केवल इसके तत्वों के प्रतिनिधित्व में है, यदि आप प्रतीकों को प्रतीकों से बदलते हैं, तो सार नहीं बदलेगा।

पासवर्ड टाइप करते समय प्रत्येक आइकन 0 से 7 तक की संख्या के अनुरूप होता है:
01234567

यह गणना करना आसान है कि हमारे पास 8 12 संयोजन हैं, हालांकि खेल केवल पूर्ण किए गए स्तरों और उपलब्ध वेशभूषा के बारे में जानकारी संग्रहीत करता है। केवल 5 स्तर (अंतिम गणना नहीं), जिसे यादृच्छिक क्रम में और 4 पोशाकों में पूरा किया जा सकता है। यानी क्रमशः 5 बिट्स और 4 बिट्स, कुल 9 बिट्स। पासवर्ड की क्षमता 12 × लॉग 2 8 है, अर्थात। 36 बिट्स पर्याप्त से अधिक है।

पासवर्ड जेनरेशन को शुरू करते हुए, गेम, हमेशा की तरह, एक सरणी बनाता है। इस बार तुरंत 12 तत्वों से, जिनमें से प्रत्येक एक पासवर्ड सेल से मेल खाती है। प्रत्येक सेल में 3 बिट्स की क्षमता होती है, और गेम उनमें 2 बिट्स की वैल्यू लिखता है, जिससे चेकसम के लिए कम से कम महत्वपूर्ण बिट होता है।

  uint8 pass[12] = {}; //     pass[7] = (clearedLevelsMask & 0x3) << 1; //    pass[9] = (clearedLevelsMask & 0xC) >>> 1; //    pass[11] = (clearedLevelsMask & 0x10) >>> 2; //     pass[8] = (suitsMask & 0x3) << 1; //     pass[10] = (suitsMask & 0xC) >>> 1; 

फिर एक 6-बिट चेकसम माना जाता है, जो कि सरणी के सभी तत्वों का अंकगणित योग है। यह राशि तब कोशिकाओं के आरक्षित कम-क्रम बिट्स में बिट्स में लिखी जाती है।

  uint8 calcChecksum(const uint8* pass) { uint8 checksum = 0; for (int i = 0; i < 12; i++) { checksum += pass[i]; } for (int i = 0; i < 6; i++) { pass[i + 6] |= (checksum >> i) & 1; } } 

परिणाम लगभग निम्नलिखित योजना है:


डेटा तैयार किया गया है, अगला चरण 5 तालिकाओं में से एक के अनुसार एक क्रमचय है। क्रिप्टोग्राफी के साथ संबद्ध है, है ना? पारित स्तरों के मुखौटा के आधार पर, एक क्रमचय तालिका का चयन किया जाता है। तालिकाओं में उनके नए आदेश के अनुसार तत्वों के अनुक्रमित होते हैं।

  char s_swizzleTableFinal[] = {0, 6, 5, 4, 10, 1, 9, 3, 7, 8, 2, 11}; char s_swizzleTables[][] = { {0, 2, 3, 1, 4, 6, 9, 5, 7, 8, 10, 11}, {8, 2, 3, 6, 10, 1, 9, 5, 7, 0, 4, 11}, {5, 4, 3, 10, 6, 0, 9, 8, 7, 1, 2, 11}, {3, 4, 1, 2, 6, 5, 9, 10, 7, 8, 0, 11} }; void swizzlePassword(uint8* pass, uint8 clearedLevelsMask) { const uint8* swizzTable = (clearedLevelsMask == 0x1F) ? s_swizzleTableFinal : s_swizzleTables[clearedLevelsMask % 4]; uint8 swizzledPass[12] = {}; for (var i = 0; i < 12; i++) { swizzledPass[i] = pass[swizzTable[i]]; } for (var i = 0; i < 12; i++) { pass[i] = swizzledPass[i]; } } 

अंतिम चरण वेतन वृद्धि तालिका का उपयोग करना है। यानी प्रत्येक कक्ष तालिका modulo के इसी तत्व के साथ अभिव्यक्त किया गया है। यह इस बात के लिए धन्यवाद है कि आइकन समान मूल्यों के साथ भी भिन्न होंगे।

  void applyIncrementTable(uint8* pass) { for (var i = 0; i < 12; i++) { pass[i] = (pass[i] + s_incrementTable[i]) % 8; } } 

इसलिए हमारे पास एक तैयार पासवर्ड है। और यह पता चला है कि 36 के 15 बिट्स इस पासवर्ड में उपयोग किए जाते हैं। लेकिन क्यों?

तथ्य यह है कि ये बिट्स अप्रयुक्त नहीं हैं। पासवर्ड प्रक्रिया डिकोडिंग एकत्र ई एल और बोनस है, साथ ही उनकी वर्तमान संख्या के बारे में उन्हें जानकारी से लेता है, लेकिन उसके बाद जांच करता है कि इन मूल्यों को शून्य कर रहे हैं। इससे हम यह मान सकते हैं कि मूल रूप से इस जानकारी को बचाने की योजना बनाई गई थी, लेकिन फिर इसे छोड़ने का निर्णय लिया गया।

क्या यह गेमप्ले को संतुलित करने का परिणाम है, या केवल एक अक्षम डेवलपर टूल है, अज्ञात है। इसके बारे में यहाँ और पढ़ें।

चर लंबाई


कहीं न कहीं मेरे पास एक नोटबुक है, जो मेरे बचपन के मुख्य आरपीजी के पासवर्ड के साथ चित्रित की गई है - लिटिल निंजा ब्रदर्स। इस तथ्य के बावजूद कि यह खेल कुछ भी अलौकिक नहीं है, इसे पूरा करने में मुझे कई साल लग गए। आखिरकार, यह मेरा पहला आरपीजी था, और पहली बार में मुझे यह भी पता नहीं था कि वहां "स्विंग" करना संभव है, इसलिए दूसरे बॉस को बिना पम्पिंग के हराने के लिए लगभग छह महीने लग गए (क्योंकि एक साथ खेलने का अवसर है)।

एक समय, यह यह गेम था जिसने मुझे पासवर्ड सिस्टम की संरचना के बारे में सोचा था - एक बार, पूरे दिन, मैं टीवी के सामने बैठा था, पासवर्ड में पैटर्न की तलाश कर रहा था और वर्तमान विशेषताओं पर उनकी निर्भरता का निर्धारण करने की कोशिश कर रहा था। नतीजतन, मैंने एक पासवर्ड लेने और पैसे की मात्रा बढ़ाने में भी कामयाबी हासिल की, लेकिन यह एकमात्र अच्छा मामला था।

थोड़ी देर के बाद, मुझ में एक आईटी विशेषज्ञ बनने की प्रक्रिया में, मुझे एक बार उस घटना के बारे में याद आया। और क्योंकि वह बहुत जिज्ञासु और प्यार से उसके सिर तोड़ था, विश्वविद्यालय की छुट्टियों जो कुछ में पासवर्ड की पीढ़ी के तंत्र का पता लगाने के लिए किया गया था के दौरान फैसला किया। अब मेरे पास एक दिन के लिए पर्याप्त होता, लेकिन फिर पूरे एक हफ्ते का समय लगता, लेकिन फिर भी, लक्ष्य हासिल किया गया।

यह मामला पिछले वाले की तुलना में अधिक दिलचस्प है, अगर केवल इसलिए कि पासवर्ड की एक चर लंबाई है: जैसा कि आप खेल के माध्यम से प्रगति करते हैं, नए वर्ण जोड़े जाते हैं। इसके अलावा, पासवर्ड संयुक्त पिछले सभी की तुलना में बहुत अधिक डेटा संग्रहीत करता है। जैसा कि आप स्क्रीनशॉट से देख सकते हैं, वर्णमाला 32 वर्ण है, और अधिकतम पासवर्ड की लंबाई 54 वर्ण है। यह हमें अधिकतम लंबाई के 32 54 संभावित पासवर्ड देता है, और चर लंबाई को ध्यान में रखते हुए 32 1 + 32 2 + ... + 32 54 विकल्प हैं। यदि आप उस जानकारी की मात्रा की गणना करते हैं जो अधिकतम लंबाई के एक पासवर्ड को समायोजित कर सकती है, तो यह 270 बिट्स (लॉग 2 32 54 ) होगा।

तो पासवर्ड किस डेटा को स्टोर करता है? चूंकि यह एक आरपीजी है, इसमें बहुत सारी विशेषताएं हैं, और उनमें से लगभग सभी को संरक्षित करने की आवश्यकता है।

सुविधा सूची




विशेषताएं:
  • चरित्र स्तर (अधिकतम 50)
  • अनुभव की राशि (अधिकतम 65535)
  • अधिकतम स्वास्थ्य (अधिकतम 255)
  • धन राशि (अधिकतम 99999)
  • एम-बोनस की संख्या (अधिकतम 6)

संगठन:
  • प्राप्त प्रिज्म की घंटियाँ (लाल, नारंगी, पीला, हरा, नीला, नीला, बैंगनी)
  • उपलब्ध कलाकृतियाँ (मारक, मन)
  • हड़ताल का प्रकार ("लोहे के पंजे", "क्रशिंग ब्लो", "मेगा-स्ट्राइक", "फायर स्ट्राइक", "ब्लंट स्ट्राइक", "गोल्डन पंजे", "ली स्ट्राइक", "प्रिज्मीय पंजे")
  • मौजूदा तलवार ("बाज़ तलवार", "बाघ तलवार", "ईगल तलवार", "प्रिज्मीय तलवार")
  • शील्ड ("स्कैली", "मिरर", "उग्र", "प्रिज़मैटिक")
  • रोबा ("सफेद", "काला", "रोब ली", "पवित्र बागे")
  • तालिस्मान ("α", "man", ",", "(", "ω")
  • एमुलेट ("I", "II", "III", "IV")
  • दीपक का प्रकार (मिलान, मोमबत्ती, मशाल, सूरज का टुकड़ा)
  • shuriken के उपलब्ध प्रकार ( "एकल", "धारावाहिक", "बूमरैंग", "फिक्सर" (परिवर्तित करने में असमर्थ))

आइटम और अधिक:
  • बन्स की संख्या (एक प्रकाश चिकित्सा औषधि का एनालॉग, 8 पीसी तक।)
  • मांस रोल की संख्या (एक मजबूत चिकित्सा औषधि का एनालॉग, 1 पीसी तक)।
  • हेलीकॉप्टरों की संख्या (शहर का पोर्टल, 8 पीसी तक)।
  • दवाओं की मात्रा (1 पीसी तक दूसरे खिलाड़ी को फिर से जीवित करने की अनुमति देता है।)
  • स्केटबोर्ड की संख्या (आपको 8 पीसी तक युद्ध के मैदान से भागने की अनुमति देता है।)
  • बमों की संख्या (8 पीसी तक)
  • क्या कोई ड्रैगस्टर है (यह ऐसी रेसिंग कार है)
  • ड्रैगस्टर के लिए बैटरी की संख्या
  • दोनों खिलाड़ियों के लिए विशेष हिट्स की उपलब्ध संख्या

वास्तव में, यह सभी डेटा सहेजा नहीं गया है, और सभी संग्रहीत डेटा सूचीबद्ध नहीं है। शहरों का दौरा किया, वर्तमान स्थान और बिट के एक जोड़े के साथ खेल की घटनाओं के बारे में जानकारी है कि उल्लेख नहीं किया गया है। बदले में, स्तर और स्वास्थ्य संरक्षित नहीं है, क्योंकि ये दोनों मूल्य सीधे अनुभव की मात्रा पर निर्भर करते हैं और अनावश्यक डेटा हैं। इसके अलावा, दूसरे खिलाड़ी के विशेष हिट की संख्या को बचाया नहीं गया है, क्योंकि यह अनिवार्य रूप से वैकल्पिक है।

तो यह सब कैसे काम करता है? के साथ शुरू करने के लिए, खेल चर की बाइट्स को इंगित करता है जो सहेजे जाने की आवश्यकता है। इन बिंदुओं के अनुसार, खेल बाइट्स की एक सरणी बनाता है, जो तब एन्कोड किया जाएगा। इस सरणी को 8 बाइट्स के 4 समूहों (अंतिम समूह में 6 बाइट्स) में विभाजित किया गया है।

  const char s_groupsBytes[4] = {8, 8, 8, 6}; const char* s_passDataMap[30] = { // Group 1 &currLocation, &bells, &moneyLo, &expLo &moneyMed, &expHi, &moneyHi, &kicks, // Group 2 &visitedCitiesLo, &visitedCitiesHi, &mBonusCount, &tStarsTypes, &punch, &usedTreasures, &tStars, &treasures, // Group 3 &sword, &bombs, &shield, &skboards, &robe, &dragster, &talisman, &meatbuns, // Group 4 &amulet, &sweetbuns, &light, &batteries, &whirlyBirds, &medicine }; 

पासवर्ड को यथासंभव कॉम्पैक्ट बनाने के लिए, सभी अशक्त मूल्यों की अनदेखी की जाती है। इस उद्देश्य के लिए, सरणी के चार समूहों में से प्रत्येक के लिए गैर-शून्य मानों का एक मुखौटा संकलित किया जाता है - एक बाइट, जहां i-th बिट एक संकेतक है कि क्या सरणी में i-th तत्व को शामिल किया जाना चाहिए। गठित सरणी में, यह मुखौटा उस समूह से पहले जाएगा जहां यह मेल खाती है।

  uint8 valuesMask(const uint8* data, int group) { uint8 valuesMask = 0; const int startIndex = group * 8; for (int i = startIndex + s_groupsBytes[group] - 1; i >= startIndex; i--) { valuesMask <<= 1; if (data[i] != 0) { valuesMask |= 1; } } return valuesMask; } [समूह] -  uint8 valuesMask(const uint8* data, int group) { uint8 valuesMask = 0; const int startIndex = group * 8; for (int i = startIndex + s_groupsBytes[group] - 1; i >= startIndex; i--) { valuesMask <<= 1; if (data[i] != 0) { valuesMask |= 1; } } return valuesMask; } 

अंत में एक ही ऑपरेशन उस स्थिति में भी समूहों पर लागू होता है जब सभी समूह मान शून्य के बराबर होते हैं: एक चार-बिट मुखौटा (एक बाइट के 4 उच्च-क्रम वाले बिट्स) होता है, जहां i-th बिट (उच्चतम से सबसे कम) इंगित करता है कि पासवर्ड शामिल है सरणी के ith समूह। यह मुखौटा हेडर में लिखा जाएगा, सीधे इन समूहों के सामने।

इसके अलावा, एक लंबाई तालिका है, जहां सरणी के प्रत्येक बाइट इसकी महत्वपूर्ण संख्या बिट्स से मेल खाती है। यानी परिणामस्वरूप, सरणी के पूरे बाइट्स को एन्कोड नहीं किया जाएगा, लेकिन केवल उपयोग किए गए मानों के बिट्स। एक ही तकनीक गैर-शून्य मान मास्क पर लागू होती है - समूह में प्रयुक्त बाइट्स की संख्या के अनुरूप उपयोग किए जाने वाले मास्क बिट्स की संख्या।

अंतिम उत्पन्न सरणी बिट स्टैक का एक एनालॉग है: चर "सामने प्रतिस्थापन" द्वारा जोड़ा जाता है - एन जोड़ा बिट्स के लिए, एन द्वारा एक तार्किक सही बदलाव पूरे सरणी पर लागू होता है, और चर का मूल्य शुरुआत में मुक्त किए गए बिट्स को लिखा जाता है।



  const char s_bitLengths[] = { 8, 7, 8, 8, 8, 8, 1, 7, 8, 2, 3, 4, 4, 2, 4, 2, 3, 4, 3, 4, 3, 1, 3, 1, 3, 4, 3, 4, 4, 1 }; void pushBits(uint8* data, uint8 value, int bitCount) { shiftRight(data, bitCount); writeBits(data, value, bitCount); } // ... uint8 encodedData[30] = {}; uint8 groupsMask = 0; for (int i = 3; i >= 0; i--) { groupsMask >>= 1; uint8 currMask = valuesMask(passData, i); if (currMask != 0) { groupsMask |= 0x80; const uint8 valuesCount = s_groupsBytes[i]; const int startIndex = i * 8; for (int j = startIndex + valuesCount - 1; j >= startIndex; j--) { if (passData[j] != 0) { pushBits(encodedData, passData[j], s_bitLengths[j]); } } pushBits(encodedData, currMask, valuesCount); } } 

फिर, 4 बाइट्स, जो एक प्रकार के हेडर हैं, सरणी की शुरुआत में जोड़े जाते हैं।यह चेकसम, पासवर्ड और इंक्रीमेंट में शामिल समूहों के मास्क को संग्रहीत करेगा - 0 से 31 तक 8-बिट यादृच्छिक मान, जो कि वर्णों के मानों में जोड़ा जाएगा modulo 32।

सबसे पहले, एक हेडर हेडर के अंत में लिखा जाता है, और, हेडर के अंतिम बाइट के साथ शुरू होता है, और। चेकसम माना जाता है। यह 20-बिट हैश है, जो कि उनके सीरियल नंबर से गुणा किए गए तत्वों का योग है।

  uint32 calcChecksum(uint8* data, int count) { uint32 sum = 0; for(int i = 0; i < count; i++) { sum += data[i] * (i + 1); } return sum; } // ... const uint8 increment = rand() % 32; shiftRight(encodedData, 32); encodedData[3] = increment; uint32 checksum = calcChecksum(&encodedData[3], (encodedDataBitLength + 7) / 8); encodedData[0] = checksum & 0xFF; encodedData[1] = (checksum >> 8) & 0xFF; encodedData[2] = ((checksum >> 16) & 0xF) | groupsMask; 

उसके बाद, डेटा, पिछले मामले में, बेस 32 के समान एन्कोडेड है। इस मामले में, पहले, चेकसम से हेडर और पासवर्ड में शामिल समूहों के मुखौटा के चार उच्च-क्रम बिट्स को अलग-अलग एन्कोड किया गया है, और फिर वेतन वृद्धि एक अलग पासवर्ड प्रतीक के साथ लिखी गई है, और उसके बाद ही अन्य सभी डेटा एन्कोडेड है।

  uint8 password[54] = {}; uint8* header = encodedData; uint8* body = &encodedData[4]; // Encode header (3 bytes + increment = 6 chars) for (int i = 0; i < 5; i++) { password[i] = takeBits(header, 3, 5); } password[5] = increment; const int charCount = (((byteCount + 1) * 8 + 4) / 5) - 1; // Encode password data for (var i = 0; i < charCount; i++) { password[i + 6] = takeBits(body, byteCount, 5); } 

वृद्धि के परिणामस्वरूप मूल्यों पर लागू किया जाता है, प्रतीक के अपवाद के साथ, जो स्वयं अपने मूल्य को संग्रहीत करता है।

  // Apply increment skipping increment char for (var i = 0; i < password.length; i++) { if (i != 5) { password[i] = (password[i] + increment) % 32; } } 

और, वास्तव में, अंतिम चरण: पाठ में वर्णानुक्रम में रूपांतरण।

  const wchar_t* s_passwordCharTable = L"—BCD\u25a0FGH+JKLMN\u25cfPQRST\u25b2VWXYZ234567"; for (int i = 0; i < charCount; i++) { std::cout << s_passChars[password[i]]; if (i % 6 == 5) { std::cout << ' '; } } 


गेम में एल्गोरिथम के कार्यान्वयन की सुविधाओं के बारे में थोड़ा सा
NES , , , . , «» .

: , ( «Base32») 4- . .

, , 9 ( ) — 8 + . , , .. 18-, , . , , .

0x12 (, 4 ), , . क्योंकि , , .

  uint8 data[32] = {}; for (int index = 0; index < 34; index++) { register = index; if (register == 0 && !(mask & 0x80)) { register = 9; index = register; } if (register == 9 && !(mask & 0x40)) { register = 18; //  ! mask = register; } if (register == 18 && !(mask & 0x20)) { //        register = 27; index = register; } if (register == 27 && !(mask & 0x10)) { return; } decodeValue(input, &data[index]); } 

, . ? , , , , , 3- , ( ), , , .

- , , ! : , . — , .


एक बोनस के रूप में


जावास्क्रिप्ट के प्रति उत्साही विशेष रूप से
एडवेंचर आईलैंड 4 (ताकाहाशी मीजिन नो बाउकेन जिमा IV) और पावर ब्लेड 2 के लिए लेख के लिए पासवर्ड जनरेट कर सकते हैं , साथ ही लगभग पांच साल पहले लिटिल निंजा के लिए जनरेटर के एनबॉल्ड संस्करण

मैं आपसे कोड को कड़ाई से नहीं आंकने के लिए कहता हूं; वेब प्रोग्रामिंग मेरी विशेषज्ञता से बहुत दूर है।

राजकुमार फारस के लिए पासवर्ड जनरेटर यहां पाया जा सकता है

और पुरानी यादों का एक छोटा सा ...













संदर्भ


Source: https://habr.com/ru/post/In150958/


All Articles