BÀI TẬP LỚN MẬT MÃ VÀ AN TOÀN DỮ LIỆU VẤN ĐỀ TÍNH TOÁN VỚI CÁC SỐ LỚN Vấn đề tính toán với các số lớn có ý nghĩa rất lớn trong thực tế. Chẳng hạn như thuật toán mã hóa công khai RSA (do Rivers, Shamir và Adleman viết ra vào năm 1978 ) sử dụng tới 512 số khóa (thuật toán này có liên quan tới việc phân tích các số nguyên tố). Trong nhiều ngành khoa học kĩ thuật chúng ta phải sử dụng tới các con số lớn hơn thế rất nhiều.
ĐẠI HỌC QUỐC GIA HÀ NỘI TRƯỜNG ĐẠI HỌC CÔNG NGHỆ CỘNG HÒA XÃ HỘI CHỦ NGHĨA VIỆT NAM Độc lập - Tự do - Hạnh phúc BÀI TẬP LỚN MÔN HỌC: MẬT MÃ VÀ AN TOÀN DỮ LIỆU ĐỀ TÀI: VẤN ĐỀ TÍNH TOÁN VỚI CÁC SỐ LỚN Họ tên: Trần Thị Mùi. Lớp: Cao học K20 Mã học viên: 13025086 Giảng viên hướng dẫn: PGS.TS. Trịnh Nhật Tiến Hà nội, 2014 1 I. Giới thiệu Vấn đề tính toán với các số lớn có ý nghĩa rất lớn trong thực tế. Chẳng hạn như thuật toán mã hóa công khai RSA (do Rivers, Shamir và Adleman viết ra vào năm 1978 ) sử dụng tới 512 số khóa (thuật toán này có liên quan tới việc phân tích các số nguyên tố). Trong nhiều ngành khoa học kĩ thuật chúng ta phải sử dụng tới các con số lớn hơn thế rất nhiều. Một số vấn đề liên quan tới việc tính toán với các số lớn như: kiểm tra tính nguyên tố của một số lớn , khai căn bậc hai của số lớn, vấn đề tính lũy thừa với số lớn theo modulo, vấn đề tính phần tử nghịch đảo theo modulo, vấn đề tính giai thừa số lớn, vấn đề các phép toán trên số lớn… Đặt Vấn Đề: Trong các ngôn ngữ lập trình cấp cao như Pascal, C, C#, … hỗ trợ kiểu dữ liệu số nguyên và các phép toán trên số học. Nhưng, độ lớn của chữ số có giới hạn. Trong Pascal, một số nguyên được lưu trữ tối đa là 32 bit (4 byte), trong C# là 64 bit (8 byte). Do đó, để có thể thực hiện được các phép toán số học với các số lớn vượt ra khỏi giới hạn kiểu dữ liệu số của ngôn ngữ lập trình, người lập trình cần xây dựng riêng cho mình một cấu trúc dữ liệu có thể lưu được số lớn, và các phép toán số học trên cấu trúc dữ liệu đó. Sau đây, em xin trình bày một phương pháp xây dựng số lớn và các phép toán số học trên số lớn trên C#. II. Bài toán 1. Phát biểu bài toán Bài toán đặt ra: cho hai số nguyên lớn a và b có độ dài lần lượt là m và n (chứa m và n chữ số). Thực hiện hai nhiệm vụ: • Xử lý dữ liệu với hai số nguyên lớn a và b: vì hai số này vượt qua khỏi giới hạn của các kiểu dữ liệu số mà máy hỗ trợ. Vì vậy ta phải thực hiện đưa nó về các kiểu dữ liệu số mà máy có thể hỗ trợ để thực hiện cho việc tính toán. • Thực hiện các phép toán với hai số: phép toán cộng, trừ, nhân, chia. 2 2. Thực hiện các bước của bài toán 2.1. Xử lý dữ liệu - Đưa dữ liệu về các kiểu số nguyên đã có mà máy hỗ trợ sẵn để tạo ra kiểu số lớn. - Tư tưởng: thực hiện biểu diễn thành các block, mỗi block gồm 4 byte - Thực hiện: đưa các chữ số vào trong các block, được thực hiện bởi BigInteger(string value, int radix), trong đó value là giá trị truyền vào, radix là hệ cơ số. - Xác đinh bởi công thức: result = result + (multiplier * posVal); multiplier = multiplier *radix; - Xử lý dữ liệu được xác định bởi thủ tục như sau: public BigInteger(string value, int radix) { BigInteger multiplier = new BigInteger(1); BigInteger result = new BigInteger(); value = (value.ToUpper()).Trim(); int limit = 0; if(value[0] == '-') limit = 1; for(int i = value.Length - 1; i >= limit ; i ) { int posVal = (int)value[i]; if(posVal >= '0' && posVal <= '9') posVal -= '0'; else if(posVal >= 'A' && posVal <= 'Z') posVal = (posVal - 'A') + 10; else posVal = 9999999; // arbitrary large if(posVal >= radix) throw(new ArithmeticException("Invalid string in constructor.")); else { if(value[0] == '-') posVal = -posVal; result = result + (multiplier * posVal); 3 if((i - 1) >= limit) multiplier = multiplier * radix; } } if(value[0] == '-') // negative values { if((result.data[maxLength-1] & 0x80000000) == 0) throw(new ArithmeticException("Negative underflow in constructor.")); } else // positive values { if((result.data[maxLength-1] & 0x80000000) != 0) throw(new ArithmeticException("Positive overflow in constructor.")); } data = new uint[maxLength]; for(int i = 0; i < result.dataLength; i++) data[i] = result.data[i]; dataLength = result.dataLength; } Khi thực hiện xử lý dữ liệu theo kiểu như trên có một ưu điểm hơn so với trường hợp ta đưa các chữ số về dạng String. Nếu theo dạng như String thì mỗi một chữ số sẽ được lưu trong một block, còn trương hợp như trên, trong mỗi một block ta lưu được nhiều chữ số, vì thế sẽ không gây ra trường hợp lãng phí bộ nhớ và thời gian thực hiện nhanh hơn. 2.2. Thực hiện các phép toán 2.2.1. Phép toán cộng - Thực hiện cộng trong mỗi một block, thực hiện công như phép tính cộng bình thường - Sử dụng một biến carry để lưu trữ biến nhớ. Khi đó kết quả của phần tử thứ i sẽ là: long sum = (long)bi1.data[i] + (long)bi2.data[i] + carry; carry = sum >> 32; result.data[i] = (uint)(sum & 0xFFFFFFFF); - Trước khi ra kết quả trả về là một số lớn có giá trị bằng tổng của hai số, sau khi thực hiện cộng tổng hai số ta thực hiện and nó trong hệ hexa và cuối cùng cho ra kết quả trả về. - Thủ tục như sau: 4 public static BigInteger operator +(BigInteger bi1, BigInteger bi2) { BigInteger result = new BigInteger(); result.dataLength = (bi1.dataLength > bi2.dataLength) ? bi1.dataLength : bi2.dataLength; long carry = 0; for(int i = 0; i < result.dataLength; i++) { long sum = (long)bi1.data[i] + (long)bi2.data[i] + carry; carry = sum >> 32; result.data[i] = (uint)(sum & 0xFFFFFFFF); } if(carry != 0 && result.dataLength < maxLength) { result.data[result.dataLength] = (uint)(carry); result.dataLength++; } while(result.dataLength > 1 && result.data[result.dataLength-1] == 0) result.dataLength ; // overflow check int lastPos = maxLength - 1; if((bi1.data[lastPos] & 0x80000000) == (bi2.data[lastPos] & 0x80000000) && (result.data[lastPos] & 0x80000000) != (bi1.data[lastPos] & 0x80000000)) { throw (new ArithmeticException()); } return result; 2.2.2. Phép toán trừ - Thực hiên trong mỗi một block và thực hiện trừ như phép trừ bình thường. - Sử dụng một biến carry để lưu trữ biến nhớ: + Nếu kết quả nhỏ hơn không: carry=1 + Ngược lại: carry=0. - Kết quả của phần tử thứ i sẽ là: long diff = (long)bi1.data[i] - (long)bi2.data[i] -carry; 5 result.data[i] = (uint)(diff & 0xFFFFFFFF); - Thủ tuc thực hiện như sau: public static BigInteger operator -(BigInteger bi1, BigInteger bi2) { BigInteger result = new BigInteger(); result.dataLength = (bi1.dataLength > bi2.dataLength) ? bi1.dataLength : bi2.dataLength; long carryIn = 0; for(int i = 0; i < result.dataLength; i++) { long diff; diff = (long)bi1.data[i] - (long)bi2.data[i] - carryIn; result.data[i] = (uint)(diff & 0xFFFFFFFF); if(diff < 0) carryIn = 1; else carryIn = 0; } // roll over to negative if(carryIn != 0) { for(int i = result.dataLength; i < maxLength; i++) result.data[i] = 0xFFFFFFFF; result.dataLength = maxLength; } // fixed in v1.03 to give correct datalength for a - (-b) while(result.dataLength > 1 && result.data[result.dataLength-1] == 0) result.dataLength ; // overflow check int lastPos = maxLength - 1; if((bi1.data[lastPos] & 0x80000000) != (bi2.data[lastPos] & 0x80000000) && (result.data[lastPos] & 0x80000000) != (bi1.data[lastPos] & 0x80000000)) { 6 throw (new ArithmeticException()); } return result; } 2.2.3. Phép nhân - Nhân từng phần tử của số b với số a, kết quả thu được lưu vào trong mang data[k]. - Kết quả mỗi phần tử thứ i được xác định bởi công thức như sau: ulong Val= bi1.data[i]*bi2.data[j]+ result.data[k]+mcarry; rusult.data[k]= (unit)(Val&0xFFFFFFFF) mcarry=(val>>32) - Thủ tục thực hiện như sau: public static BigInteger operator *(BigInteger bi1, BigInteger bi2) { int lastPos = maxLength-1; bool bi1Neg = false, bi2Neg = false; // take the absolute value of the inputs try { if((bi1.data[lastPos] & 0x80000000) != 0) // bi1 negative { bi1Neg = true; bi1 = -bi1; } if((bi2.data[lastPos] & 0x80000000) != 0) // bi2 negative { bi2Neg = true; bi2 = -bi2; } } catch(Exception) {} BigInteger result = new BigInteger(); // multiply the absolute values try { for(int i = 0; i < bi1.dataLength; i++) { if(bi1.data[i] == 0) continue; 7 ulong mcarry = 0; for(int j = 0, k = i; j < bi2.dataLength; j++, k++) { // k = i + j ulong val = ((ulong)bi1.data[i] * (ulong)bi2.data[j]) + (ulong)result.data[k] + mcarry; result.data[k] = (uint)(val & 0xFFFFFFFF); mcarry = (val >> 32); } if(mcarry != 0) result.data[i+bi2.dataLength] = (uint)mcarry; } } catch(Exception) { throw(new ArithmeticException("Multiplication overflow.")); } result.dataLength = bi1.dataLength + bi2.dataLength; if(result.dataLength > maxLength) result.dataLength = maxLength; while(result.dataLength > 1 && result.data[result.dataLength-1] == 0) result.dataLength ; // overflow check (result is -ve) if((result.data[lastPos] & 0x80000000) != 0) { if(bi1Neg != bi2Neg && result.data[lastPos] == 0x80000000) // different sign { // handle the special case where multiplication produces // a max negative number in 2's complement. if(result.dataLength == 1) return result; else { bool isMaxNeg = true; 8 for(int i = 0; i < result.dataLength - 1 && isMaxNeg; i++) { if(result.data[i] != 0) isMaxNeg = false; } if(isMaxNeg) return result; } } throw(new ArithmeticException("Multiplication overflow.")); } // if input has different signs, then result is -ve if(bi1Neg != bi2Neg) return -result; return result; } 2.2.4. Phép chia - Thực hiện với hai trường hợp: + Nếu b chỉ có 1 block: ta thực hiện chia như bình thường, kiểm tra độ dài của a, nếu nếu a nhỏ hơn b thì ta cho ra kết quả thương= 0 và số dư sẽ là kết quả của a. Nếu a > b ta thực hiện: ta xét từng phần nhỏ trong a dividend = ((ulong)outRemainder.data[pos+1] << 32) + (ulong)outRemainder.data[pos]; ulong quotient = dividend / divisor; result[resultPos++] = (uint)quotient; outRemainder.data[pos+1] = 0; outRemainder.data[pos ] = (uint)(dividend % divisor); Sau khi thực hiện chia xong lại kiểm tra tiếp giá trị của a là số outRemainder[] (biến phần nhỏ của a), nếu nhỏ hơn ta tiến hành dịch bít a và thêm vào sau phần nhỏ của a Ta có thủ tục: private static void singleByteDivide(BigInteger bi1, BigInteger bi2, BigInteger outQuotient, BigInteger outRemainder) { 9 uint[] result = new uint[maxLength]; int resultPos = 0; // copy dividend to reminder for(int i = 0; i < maxLength; i++) outRemainder.data[i] = bi1.data[i]; outRemainder.dataLength = bi1.dataLength; while(outRemainder.dataLength > 1 && outRemainder.data[outRemainder.dataLength-1] == 0) outRemainder.dataLength ; ulong divisor = (ulong)bi2.data[0]; int pos = outRemainder.dataLength - 1; ulong dividend = (ulong)outRemainder.data[pos]; //Console.WriteLine("divisor = " + divisor + " dividend = " + dividend); //Console.WriteLine("divisor = " + bi2 + "\ndividend = " + bi1); if(dividend >= divisor) { ulong quotient = dividend / divisor; result[resultPos++] = (uint)quotient; outRemainder.data[pos] = (uint)(dividend % divisor); } pos ; while(pos >= 0) { //Console.WriteLine(pos); dividend = ((ulong)outRemainder.data[pos+1] << 32) + (ulong)outRemainder.data[pos]; ulong quotient = dividend / divisor; result[resultPos++] = (uint)quotient; outRemainder.data[pos+1] = 0; outRemainder.data[pos ] = (uint)(dividend % divisor); //Console.WriteLine(">>>> " + bi1); } outQuotient.dataLength = resultPos; 10 [...]... outRemainder.data[outRemainder.dataLength-1] == 0) outRemainder.dataLength ; } + Trường hợp b có nhiều hơn 1 block: ta xét từng phần nhỏ trong a, phần nhỏ nhận giá trị và dừng lại khi lớn hơn b Thực hiện trừ dần phần nhỏ đã xét với b, được bao nhiêu lưu vào trong một biến tạm, sau khi thực hiện xong phép trừ ta được kết quả là thương của phép chia Ta có thủ tục: private static void multiByteDivide(BigInteger . modulo, vấn đề tính giai thừa số lớn, vấn đề các phép toán trên số lớn Đặt Vấn Đề: Trong các ngôn ngữ lập trình cấp cao như Pascal, C, C#, … hỗ trợ kiểu dữ liệu số nguyên và các phép toán trên số. liên quan tới việc tính toán với các số lớn như: kiểm tra tính nguyên tố của một số lớn , khai căn bậc hai của số lớn, vấn đề tính lũy thừa với số lớn theo modulo, vấn đề tính phần tử nghịch. NAM Độc lập - Tự do - Hạnh phúc BÀI TẬP LỚN MÔN HỌC: MẬT MÃ VÀ AN TOÀN DỮ LIỆU ĐỀ TÀI: VẤN ĐỀ TÍNH TOÁN VỚI CÁC SỐ LỚN Họ tên: Trần Thị Mùi. Lớp: Cao học K20 Mã học viên: 13025086 Giảng viên