Generating ED25519 SSH Key in Ruby - ruby

I'm looking to generate ED25519 EC key pairs in Ruby. The OpenSSL gem does not support this particular signature scheme. There is a ED25519 gem but this does not appear to generate a key pair that I can export and use.
How would (should) I generate these types of key pairs in Ruby?

So, having looked, I also didn't find an obvious answer to this. But not to despair! The SSH key format is documented and boils down to a bunch of base64ed pascal-style strings. So let's do this thing!
require 'ed25519'
require 'securerandom'
require 'base64'
# PLEASE NOTE FOR SECURITY:
# the ed25519 gem and the library it is based off of are not protected against
# timing attacks. This script will generate _secure_ keys but the computing
# environment in which they are generated must also be secure and must _not_
# be a shared environment.
# rfc4253 defines the general way this is done. for older key types.
# nothing has changed except the keytypes.
# openssh's key format is
# "ssh-keytype "+(base64 (encodekey keydata))" comment"
# and keydata is either one of these two:
# numeric key types like rsa: sshstring("ssh-rsa")+sshmpint(e)+sshmpint(n)
# byte string key types like ed25519: sshstring("ssh-ed25519")+sshstring(keymatter)
# openssh's basic types are actually just different plays on pascal strings with uint32s.
# let's implement those here
def sshstring(s)
# s = s.force_encoding("ASCII-8BIT")
# s = s.b
[
s.length >> 24 & 0xff,
s.length >> 16 & 0xff,
s.length >> 8 & 0xff,
s.length & 0xff,
].pack('c*') + s
end
def sshpaddedstring(blocksize, s)
# first, add all the padding we could need. then truncate the tail.
/^(.{#{blocksize}}*)/m.match(s + Array(1..blocksize).pack('c*'))
sshstring($1)
end
def sshmpint(i)
# first, make a string of bytes
nl = []
while i > 0 do
nl << (i & 0xff);
i >>= 8;
end
sshstring(nl.pack('c*'))
end
# now that we can generate values out of strings and numbers and we know what
# order to put them in, we can make an ed25519 public key.
def public_key_innards_from_ed25519(signing_key)
sshstring("ssh-ed25519")+sshstring(signing_key.verify_key.to_bytes)
end
def public_key_from_ed25519(signing_key, comment)
"ssh-ed25519 #{[public_key_innards_from_ed25519(signing_key)].pack 'm0'} #{comment}"
end
# that was surprisingly straightforward. now for private keys
# ... l3ll3llll3l also straightforward but a lot more work :D
# these ones are really just more of the same except for a few details
def ssh_keygen_ed25519
# to start, we need a key.
signing_key = Ed25519::SigningKey.generate
# we also need a comment
comment = "this key is a real gem"
# then we need to generate the public key format used later in the file.
# these are the innards that also go in the public key wrapper format
# which will be placed in authorized_keys. we'll save them here to
# avoid generating it over and over (typing it over and over really)
public_key_innards = public_key_innards_from_ed25519(signing_key)
private_key_blob = [
# now, let's start building the key!
"openssh-key-v1\0", # first, they start with a null terminated magic string
sshstring("none"), # the cipher name or "none" if the key is not encrypted
sshstring("none"), # the KDF name or "none" if the key is not encrypted
sshstring(""), # kdf data or an empty string if the key is not encrypted
"\x00\x00\x00\x01", # pop quiz! how many keys are in this file? it's 1. it's _always_ 1.
# next is the public key in the second field format of authorized_keys but not base64ed
sshstring(public_key_innards),
sshpaddedstring(8, # this section is an embedded sshstring padded to 8 bytes.
[
(SecureRandom.random_bytes 4) * 2,
# and some random bytes for extra randominity
# jk they're for checking the encrypted vs unencrypted values.
# there are two of them so that they can be compared after decryption.
# they will appear to be random values if decryption failes.
# and, of course, if you don't encrypt at all, they're just a random uint32s repeated
public_key_innards,
# "hello",
sshstring(signing_key.seed + signing_key.verify_key.to_str), # this is just the private key and public key concatenated...
# "hell2",
# ... ... for anyone counting, yes, you counted right.
# the public key is included in the "private key" three times. so this definitely isn't
# just the private key. it also explains why the private key is enormous compared to the
# public key despite the public key and private key being the same length (32 bytes) for
# ed25519. more info about ed25519, including how to generate _that_ can be found in rfc8032.
sshstring(comment),
].join
),
].join
# the pack line means "encode as base64 with no newlines and chunk in 70 byte lines."
[
public_key_from_ed25519(signing_key, comment),
<<~EOF
-----BEGIN OPENSSH PRIVATE KEY-----
#{([private_key_blob].pack('m0').scan(/.{1,70}/m)).join("\n").chomp}
-----END OPENSSH PRIVATE KEY-----
EOF
]
end
kg = ssh_keygen_ed25519
puts "PUBLIC KEY:\n\n#{kg[0]}\n\n"
puts "PRIVATE KEY:\n\n#{kg[1]}\n"
This code is some ruby and could be made into a gem; if you can accept or mitigate the security issues in the ed25519 gem, have at it. I think it answers the question, anyway.
But for that bit of the question in parentheses, here's what I would do in reality:
require 'tmpdir'
require 'tempfile'
# first, don't let anyone else touch these.
# these files can't be opened twice but security in depth, plx
oldumask = File.umask 0077
def ssh_keygen_ed25519
Dir.mktmpdir do
|dir|
newkey = ""
newpub = ""
keyf = File.join(dir, "key")
keyfq = "'#{keyf.sub(/'/, %{'"'"'})}'" #escape the key filename for bash later
pubf = File.join(dir, "key.pub")
File.mkfifo(keyf)
File.mkfifo(pubf)
keythread = Thread.new { File.open(keyf) {|f| newkey = f.read} }
pubthread = Thread.new { File.open(pubf) {|f| newpub = f.read} }
ssh_keygen_output = `yes y | ssh-keygen -q -t ed25519 -N '' -f #{keyfq}`
keythread.join()
pubthread.join()
return newpub, newkey
end
end
puts ssh_keygen_ed25519
This creates a temp dir, creates two fifos in it to take the contents of the keys from ssh-keygen, creates two threads to read those (the threads will complete their work when ssh-keygen closes the files and the variables will be filled when the joins return), runs ssh-keygen in the main thread and says yes to the prompt for whether to overwrite the files (the fifos), then prints the contents. Nothing too fancy and you get to use the ssh-keygen you know and love.

Related

ruby: create MD5 checksum with salt?

I'm trying to create an MD5 checksum in ruby with salt, but I cannot find any way to do this using the standard digest/md5 package.
I know I can do this:
require 'digest/md5'
checksum = '$1$' + (Digest::MD5.new << plaintext).to_s
However, there doesn't appear to be any way to specify salt for this MD5 checksum generation using digest, and I haven't found any other package I could use for this in ruby.
Is this even possible in ruby?
Creating/Validating *nix Style MD5 Entries with Salt
If you're trying to manage *nix system passwords, you're better off just shelling out to system utilities rather than building your own. However, if you want to generate or validate salted passwords with only Ruby core or standard library capabilities, you certainly can.
A computed MD5 password with salt is generally stored in a flat-file database (e.g. /etc/shadow) like this, where $ is the field separator:
$1$salt$hashed_pw
Note that the first two fields are stored in cleartext, because they're needed to rebuild and hash the correct string when presented with only the password to validate. As a result, you need to treat the salt as a separate variable from the plaintext password, although the salt is included with the password when hashed.
If you don't have a character limitation imposed by your utilites, one way to generate a strong salt is to use SecureRandom#uuid to generate a UUIDv4 value. For example:
require 'securerandom'
salt = SecureRandom.uuid
#=> "c05280ef-151c-4ebc-83c6-f5f0906f89c2"
You can then invoke your MD5 hash on salt + pw or pw + salt depending on your application's password implementation. For example:
require 'digest/md5'
MD5_STR_FMT = '$1$%s$%s'.freeze
salt = 'c05280ef-151c-4ebc-83c6-f5f0906f89c2'
pw = 'plaintext password gathered securely'
pw_digest = Digest::MD5.new << salt + pw
pw_entry = MD5_STR_FMT % [salt, pw_digest]
#=> "$1$c05280ef-151c-4ebc-83c6-f5f0906f89c2$87dcc23c0008e45526e474d0364e4aa5"
You then store pw_entry in your password database file, and then parse out the salt to prepend to the offered password when you recompute the hash during authentication. For example:
require 'digest/md5'
# this is how we'll validate a password from user
# input against an entry from the password database
def valid_pw? pw, salt, hashed_pw
pw_digest = Digest::MD5.new << salt + pw
pw_digest.to_s.eql? hashed_pw.to_s
end
# extract salt and password from a database entry
def parse_pw_entry str
str.split(?$).slice -2, 2
end
# get this from your password database in whatever
# way you like
pw_entry = '$1$c05280ef-151c-4ebc-83c6-f5f0906f89c2$87dcc23c0008e45526e474d0364e4aa5'
# for demonstration purposes only; gather password
# securely from user, then perform your validation
['foo', 'plaintext password gathered securely'].map do |pw|
valid_pw? pw, *parse_pw_entry(pw_entry)
end
#=> [false, true]
You can add compute the digest of multiple chunks like this:
require 'digest/md5'
md5 = Digest::MD5.new
md5 << '$1$'
md5 << plaintext
checksum = md5.to_s
Or by string concatenation of the salt and the text in one method call:
salt = '$1$'
checksum = Digest::MD5.hexdigest("#{salt}#{plaintext}")
I found the following, and it does what I want ...
https://github.com/mogest/unix-crypt
It works like this:
require 'unix_crypt'
checksum = UnixCrypt::MD5.build(plaintext, salt)
This generates the same checksum as is used in /etc/shadow, which is what I want to use it for,

How to Generate a Bitcoin Address in Ruby

I'm trying to generate Bitcoin addresses in ruby by using this guide:
https://bhelx.simst.im/articles/generating-bitcoin-keys-from-scratch-with-ruby/
But something isn't quite right, because the addresses being generated aren't coming out quite right.
Here's the class I'm using:
require 'openssl'
require 'ecdsa'
require 'securerandom'
require 'base58'
class BitcoinAddressGenerator
ADDRESS_VERSION = '00'
def self.generate_address
# Bitcoin uses the secp256k1 curve
curve = OpenSSL::PKey::EC.new('secp256k1')
# Now we generate the public and private key together
curve.generate_key
private_key_hex = curve.private_key.to_s(16)
puts "private_key_hex: #{private_key_hex}"
public_key_hex = curve.public_key.to_bn.to_s(16)
puts "public_key_hex: #{public_key_hex}"
pub_key_hash = public_key_hash(public_key_hex)
puts "pub_key_hash: #{pub_key_hash}"
address = generate_address_from_public_key_hash(public_key_hash(public_key_hex))
puts "address: #{address}"
end
def self.generate_address_from_public_key_hash(pub_key_hash)
pk = ADDRESS_VERSION + pub_key_hash
encode_base58(pub_key_hash + checksum(pub_key_hash))
end
def self.int_to_base58(int_val, leading_zero_bytes=0)
alpha = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
base58_val, base = '', alpha.size
while int_val > 0
int_val, remainder = int_val.divmod(base)
base58_val = alpha[remainder] + base58_val
end
base58_val
end
def self.encode_base58(hex)
leading_zero_bytes = (hex.match(/^([0]+)/) ? $1 : '').size / 2
("1"*leading_zero_bytes) + int_to_base58( hex.to_i(16) )
end
def self.checksum(hex)
sha256(sha256(hex))[0...8]
end
# RIPEMD-160 (160 bit) hash
def self.rmd160(hex)
Digest::RMD160.hexdigest([hex].pack("H*"))
end
def self.sha256(hex)
Digest::SHA256.hexdigest([hex].pack("H*"))
end
# Turns public key into the 160 bit public key hash
def self.public_key_hash(hex)
rmd160(sha256(hex))
end
end
It outputs something like:
private_key_hex: C96DE079BAE4877E086288DEDD6F9F70B671862B7E6E4FC0EC401CADB81EDF45
public_key_hex: 0422435DF80F62E643D3CFBA66194052EC9ED0DFB47A1B26A4731079A5FF84FBF98FF0A540B6981D75BA789E6192F3B38BABEF6B0286CAEB4CAFCB51BB96D97B46
public_key_hash: db34927cc5ec0066411f366d9a95f9c6369c6e1d
address: Lz3xnxx6Uh79PEzPpWSMMZJVWR36hJgVL
If I plug this address into blockchain.info and similar tools it says that it's an invalid address.
Any help would be greatly appreciated.
In your generate_address_from_public_key_hash method, the checksum should be over the hash including the address prefix. You’re not actually using the pk variable at all at the moment after you assign it. The code should look something like:
def self.generate_address_from_public_key_hash(pub_key_hash)
pk = ADDRESS_VERSION + pub_key_hash
encode_base58(pk + checksum(pk)) # Using pk here, not pub_key_hash
end
The mistake seems to also be on the page you link to, I guess the author must have made a copy/paste error.
As an aside, keeping everything in hex strings and decoding back and forth seems an odd way of doing this. I would have thought it would be easier to use raw binary strings, and only encode to hex when printing out values.

Encrypt empty string

I am using Ruby's Open SSL bindings to do AES-256 encryption. I can encrypt a non-empty string. However, when attempting to encrypt an empty string, Ruby raises an exception complaining that the data must not be empty. How can I encrypt an empty string using Ruby's OpenSSL bindings?
Code to reproduce the problem
require "openssl"
KEY = OpenSSL::Cipher::Cipher.new("aes-256-cbc").random_key
def encrypt(plaintext)
cipher = OpenSSL::Cipher::Cipher.new("aes-256-cbc")
cipher.encrypt
iv = cipher.random_iv
cipher.iv = iv
cipher.key = KEY
ciphertext = cipher.update(plaintext) # <- ArgumentError here
ciphertext << cipher.final
[iv, ciphertext]
end
def decrypt(iv, ciphertext)
cipher = OpenSSL::Cipher::Cipher.new("aes-256-cbc")
cipher.decrypt
cipher.iv = iv
cipher.key = KEY
plaintext = cipher.update(ciphertext)
plaintext << cipher.final
plaintext
end
p decrypt(*encrypt("foo")) # "foo"
p decrypt(*encrypt(""))
# /tmp/foo.rb:11:in `update': data must not be empty (ArgumentError)
# from /tmp/foo.rb:11:in `encrypt'
# from /tmp/foo.rb:27:in `<main>'
Versions
ruby-2.2.2p95
OpenSSL::VERSION is "1.1.0"
Microsoft SQL Server 2014 (12.0.2000.8)
Why do I want to encrypt empty strings?
I am writing an ETL program to migrate data from one database to a SqlServer database. Certain columns from the source database must be encrypted before writing them to the destination database. The source columns may contain any data, including empty strings. The destination columns are usually non-nullable. The destination columns will be decrypted by .net code.
Goal #1: No information about the encrypted field, including whether or not it even exists, should be recoverable without properly decrypting it. An encrypted empty string should be indistinguishable from any other encrypted data.
Goal #2: The .net code that will decrypt these values should not need to handle empty strings specially.
If I can get openssl to encrypt empty strings, I will achieve both of these goals.
Workaround - Don't encrypt empty strings
I could just not encrypt empty strings, passing them through.
def encrypt(plaintext)
return plaintext if plaintext.empty?
...
end
def decrypt(iv, ciphertext)
return ciphertext if ciphertext.empty?
...
end
This has the disadvantages of exposing information, and also of requiring cooperating code to be written on the .net side.
Workaround - Add some constant to the plaintext
I could add some constant string to the plaintext before encryption, and remove it after decryption:
PLAINTEXT_SUFFIX = " "
def encrypt(plaintext)
plaintext += PLAINTEXT_SUFFIX
...
end
def decrypt(iv, ciphertext)
...
plaintext.chomp(PLAINTEXT_SUFFIX)
end
This hides whether the data exists or not, but still requires cooperating .net code.
As suggested by #ArtjomB, it's as simple as not calling Cipher#update with the empty string. The value returned by Cipher#final then properly encrypts an empty string.
require "openssl"
KEY = OpenSSL::Cipher::Cipher.new("aes-256-cbc").random_key
def encrypt(plaintext)
cipher = OpenSSL::Cipher::Cipher.new("aes-256-cbc")
cipher.encrypt
iv = cipher.random_iv
cipher.iv = iv
cipher.key = KEY
ciphertext = ""
ciphertext << cipher.update(plaintext) unless plaintext.empty?
ciphertext << cipher.final
[iv, ciphertext]
end
def decrypt(iv, ciphertext)
cipher = OpenSSL::Cipher::Cipher.new("aes-256-cbc")
cipher.decrypt
cipher.iv = iv
cipher.key = KEY
plaintext = cipher.update(ciphertext)
plaintext << cipher.final
end
p decrypt(*encrypt("foo")) # "foo"
p decrypt(*encrypt("")) # ""
If you can use DBMS provided encryption functions, then, MySQL AES_ENCRYPT, seems to be able to encrypt blank string.
For example:
UPDATE some_table
SET some_column = AES_ENCRYPT('',UNHEX('F3229A0B371ED2D9441B830D21A390C3'));
It's AES-128 by default, I am guessing that will be a problem as you need AES-256. Also, not sure which DBMS you are using and whether that DBMS has encryption functions.

Perl AES vs Ruby AES

I cannot get Perl and Ruby to agree on CBC AES:
Perl
use Crypt::CBC;
use MIME::Base64::Perl;
my $cipher = Crypt::CBC->new(
-key => 'd2cb415e067c7b13',
-iv => 'e36dc751d0433f05', #random 16chars!!!!!! shold NOT repeat between requests
-cipher => 'OpenSSL::AES', #this is same as Rijndael
-literal_key => 1,
-header => "none",
-keysize => 16
);
$encypted = $cipher->encrypt("a really really long long text has differrent results???");
$base64 = encode_base64($encypted);
print("Ciphertext(b64): $base64");
$de_base64 = decode_base64($base64);
$decrypted = $cipher->decrypt($de_base64);
$c = $cipher->finish;
Ciphertext(b64): qz4eSQaFkQUkDOyJSbZf5W03HoldwtgvTLq0yJFRViKJnytf3PVSCGW2CYDjO+tRqV20oxeB2VPa
7NqN1TDSNQ==
there's a newline after the 2VPa section and another at the end
Ruby
require 'openssl'
require 'digest/sha2'
require 'base64'
message = "a really really long long text has differrent results???"
cipher = OpenSSL::Cipher.new('aes-128-cbc')
# digest the key, iv and hmac_key so we have 16-byte length
# also, it looks more of a funky password
# prepare cipher
cipher.encrypt
cipher.key = aes_key = "d2cb415e067c7b13"
cipher.iv = aes_iv = "e36dc751d0433f05"
encrypted = cipher.update(message)
encrypted << cipher.final()
b64_encoded = Base64.encode64(encrypted).encode('utf-8') #strict_encode64 guarantees no newlines, encode64 is default
puts "AES Key : '#{aes_key}'"
puts "AES IV : '#{aes_iv}'"
puts "Ciphertext(b64): '#{b64_encoded}'"
Ciphertext(b64): 'qz4eSQaFkQUkDOyJSbZf5W03HoldwtgvTLq0yJFRViKJnytf3PVSCGW2CYDj
O+tRqV20oxeB2VPa7NqN1TDSNQ==
'
Note the newlines chars after CYDj and after ==
I've seen the following: Perl & Ruby exchange AES encrypted information, but I'm not using padding=0
Newlines are not significant in Base64. You got exactly the same result from both languages.
While there should be absolutely no reason to do so, you could make the Perl version return the same string as the Ruby version as follows:
$base64 = encode_base64($encypted, '');
$base64 =~ s/\G.{60}\K/\n/sg;
The encode_base64 function takes a second parameter, called "eol" (end of line) which, by default, is '\n'.
The returned encoded string is broken into lines of no more than 76
characters each and it will end with $eol unless it is empty
Try:
$base64 = encode_base64($encypted, '');
instead.

Reading/Writing password protected and encrypted file in ruby

I want to encrypt a file that a ruby program will be loading data from.
In addition, I need the program to prompt for a password on startup that will be used to decrypt the file.
In other words, the file needs to reside encrypted on the machine and only users with passwords will be able to run the app.
I have started to look at openpgp but as far as I understand, this does still not solve the password problem.
There's two easy ways to go about doing this. One is to shell out to openssl to do your encryption / decryption there. The arguably better way would be to use the Ruby Crypto gem.
Program to encrypt:
require 'rubygems'
require 'crypt/blowfish';
puts "Password? "
pw = gets
puts "Secret data? "
data = gets
blowfish = Crypt::Blowfish.new(pw)
r = StringIO.new(data);
File.open('data', 'w') do |f|
while l = r.read(8) do
while l.size < 8 do l += "\0" end
f.print blowfish.encrypt_block(l)
end
end
Program to decrypt:
require 'rubygems'
require 'crypt/blowfish';
puts "Password? "
pw = gets
blowfish = Crypt::Blowfish.new(pw)
r = StringIO.new();
File.open('data', 'r') do |f|
while l = f.read(8) do
r << blowfish.decrypt_block(l)
end
end
puts "Secret data:"
puts r.string
This example uses the Blowfish symmetric block cypher. Other cyphers could be used. Also, you would probably want to concatenate a fixed string to the password, to make the key longer and to help tie the encryption/decryption to your application.
Try the encrypted strings gem. Works like a charm.

Resources