Last active
February 7, 2025 18:37
-
-
Save isaaclyman/0f0b396da7319d88f37b845f9bcaf10d to your computer and use it in GitHub Desktop.
RSA key generation, encryption, and decryption in Ruby (NOT SECURE, EDUCATIONAL PURPOSES ONLY)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
source 'https://rubygems.org' | |
ruby '3.3.4' | |
gem 'prime' | |
gem 'rspec' |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# frozen_string_literal: true | |
class RSACrypto | |
class << self | |
def encrypt(message, exponent, modulus) | |
message.bytes.map { |b| crypt_byte(b, exponent, modulus) }.join | |
end | |
def decrypt(message, exponent, modulus) | |
message.chars.each_slice(modulus.to_s.size).map do |arr| | |
# Equivalent to `(arr.join.to_i ** exponent) % modulus`, but WAY faster | |
arr.join.to_i.pow(exponent, modulus) | |
end.pack('c*') | |
end | |
private | |
def crypt_byte(byte, exponent, modulus) | |
# Equivalent to `crypted = ((byte.to_i ** exponent) % modulus).to_s` | |
crypted = byte.to_i.pow(exponent, modulus).to_s | |
missing_chars = modulus.to_s.size - crypted.size | |
'0' * missing_chars + crypted | |
end | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# frozen_string_literal: true | |
require_relative '../rsa_keygen' | |
require_relative '../rsa_crypto' | |
describe RSACrypto do | |
test_messages = [ | |
'0', | |
'TEST_MESSAGE', | |
'MY NAME IS FRED', | |
'23iorfanv84809293nioafnkl' | |
] | |
shared_examples :encrypts_decrypts_messages do | |
test_messages.each do |message| | |
it "private-encrypts and public-decrypts '#{message}'" do | |
encrypted = RSACrypto.encrypt(message, private_key_exponent, shared_modulus) | |
expect(encrypted).not_to eq(message) | |
decrypted = RSACrypto.decrypt(encrypted, public_key_exponent, shared_modulus) | |
expect(decrypted).to eq(message) | |
end | |
it "public-encrypts and private-decrypts '#{message}'" do | |
encrypted = RSACrypto.encrypt(message, public_key_exponent, shared_modulus) | |
expect(encrypted).not_to eq(message) | |
decrypted = RSACrypto.decrypt(encrypted, private_key_exponent, shared_modulus) | |
expect(decrypted).to eq(message) | |
end | |
end | |
end | |
context 'with a known good RSA key pair' do | |
let(:shared_modulus) { 25777 } | |
let(:private_key_exponent) { 3 } | |
let(:public_key_exponent) { 16971 } | |
include_examples :encrypts_decrypts_messages | |
end | |
context 'with generated key pairs' do | |
primes = [757, 1033, 1609, 2243, 2383, 3209, 5867, 7121, 7877] | |
primes.permutation(2).each do |arr| | |
context "with primes [#{arr[0]}, #{arr[1]}]" do | |
let!(:keys) { RSAKeygen.new(arr[0], arr[1]) } | |
let(:shared_modulus) { keys.public_key[1] } | |
let(:private_key_exponent) { keys.private_key[0] } | |
let(:public_key_exponent) { keys.public_key[0] } | |
include_examples :encrypts_decrypts_messages | |
end | |
end | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# frozen_string_literal: true | |
require 'prime' | |
class RSAKeygen | |
def initialize(prime_p, prime_q) | |
raise 'Both p and q must be prime' unless prime_p.prime? && prime_q.prime? | |
@prime_p = prime_p | |
@prime_q = prime_q | |
end | |
def private_key | |
@private_key ||= [private_exponent_d, modulus_n] | |
end | |
def public_key | |
@public_key ||= [public_exponent_e, modulus_n] | |
end | |
private | |
attr_accessor :prime_p, :prime_q | |
def modulus_n | |
@modulus_n ||= prime_p * prime_q | |
end | |
def totient | |
@totient ||= (prime_p - 1).lcm(prime_q - 1) | |
end | |
def public_exponent_e | |
@public_exponent_e ||= 3.upto(totient).find do |attempt| | |
attempt.prime? && totient % attempt != 0 | |
end | |
end | |
def modular_multiplicative_inverse(a, m) | |
raise "#{a} and #{m} are not coprime" unless a.gcd(m) == 1 | |
return m if m == 1 | |
m0, inv, x0 = m, 1, 0 | |
while a > 1 | |
inv -= (a / m) * x0 | |
a, m = m, a % m | |
inv, x0 = x0, inv | |
end | |
inv += m0 if inv < 0 | |
inv | |
end | |
def private_exponent_d | |
@private_exponent_d ||= modular_multiplicative_inverse(public_exponent_e, totient) | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment