2016-12-09 00:34:49 +01:00
|
|
|
#!/usr/bin/env ruby
|
|
|
|
|
|
|
|
#
|
|
|
|
# This is a simple command line script to create a set of values expected
|
2017-02-12 19:17:41 +01:00
|
|
|
# by the TREES plugin. Useful for testing.
|
2016-12-09 00:34:49 +01:00
|
|
|
#
|
|
|
|
# NOTE: this requires rbnacl-4.0.0.pre.gem or newer, or Riseup's fork of rbnacl:
|
|
|
|
#
|
|
|
|
# gem install specific_install
|
|
|
|
# gem specific_install https://0xacab.org/riseup/rbnacl
|
|
|
|
# gem install rbnacl-libsodium
|
|
|
|
#
|
|
|
|
# The gem rbnacl-libsodium is just a copy of libsodium that will built and
|
|
|
|
# install the latest libsodium in ruby's LD_LIBRARY_PATH. This is useful,
|
|
|
|
# because the packaged versions for debian stable and ubuntu are too old to
|
|
|
|
# support argon2.
|
|
|
|
#
|
|
|
|
|
|
|
|
begin
|
|
|
|
require 'rbnacl/libsodium'
|
|
|
|
require 'rbnacl'
|
|
|
|
rescue LoadError
|
|
|
|
puts "the gems rbnacl and rbnacl-libsodium are required"
|
|
|
|
exit 1
|
|
|
|
end
|
|
|
|
|
|
|
|
def usage
|
|
|
|
puts "USAGE:"
|
2017-02-12 19:17:41 +01:00
|
|
|
puts " trees-create --password PASSWORD [OPTIONS]"
|
2016-12-09 00:34:49 +01:00
|
|
|
puts
|
|
|
|
puts "OPTIONS may include:"
|
|
|
|
puts " --opslimit OPSLIMIT -- argon2 ops limit, integer in 3..10, or one of"
|
|
|
|
puts " 'interactive', 'moderate', 'sensitive'"
|
|
|
|
puts " --memlimit MEMLIMIT -- argon2 memory limit, in bytes, or one of"
|
|
|
|
puts " 'interactive', 'moderate', 'sensitive'"
|
|
|
|
puts " --salt SALT -- hex encoded salt for password digest,"
|
|
|
|
puts " #{StorageKey::SALT_BYTES} bytes in length"
|
|
|
|
puts " --nonce NONCE -- hex encoded nonce for secretbox encryption of"
|
|
|
|
puts " private key, #{StorageKey::NONCE_BYTES} bytes in length"
|
|
|
|
exit 1
|
|
|
|
end
|
|
|
|
|
|
|
|
def main
|
|
|
|
password = nil
|
|
|
|
st = StorageKey.new
|
|
|
|
|
|
|
|
while ARGV.any?
|
|
|
|
case ARGV.first
|
|
|
|
when "--password"
|
|
|
|
ARGV.shift
|
|
|
|
password = ARGV.shift
|
|
|
|
when "--opslimit"
|
|
|
|
ARGV.shift
|
|
|
|
st.pwhash_opslimit = opslimit(ARGV.shift)
|
|
|
|
when "--memlimit"
|
|
|
|
ARGV.shift
|
|
|
|
st.pwhash_memlimit = memlimit(ARGV.shift)
|
|
|
|
when "--salt"
|
|
|
|
ARGV.shift
|
|
|
|
st.pwhash_salt = ARGV.shift
|
|
|
|
when "--nonce"
|
|
|
|
ARGV.shift
|
|
|
|
st.sk_nonce = ARGV.shift
|
|
|
|
else
|
|
|
|
usage
|
|
|
|
end
|
|
|
|
end
|
|
|
|
usage unless password
|
|
|
|
st.generate_new_keypair(password)
|
|
|
|
puts st.to_s
|
|
|
|
end
|
|
|
|
|
|
|
|
def opslimit(arg)
|
|
|
|
if arg.nil?
|
|
|
|
usage
|
|
|
|
elsif arg =~ /[0-9]+/
|
|
|
|
arg.to_i
|
|
|
|
else
|
|
|
|
RbNaCl::PasswordHash::Argon2.opslimit_value(arg.to_sym)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def memlimit(arg)
|
|
|
|
if arg.nil?
|
|
|
|
usage
|
|
|
|
elsif arg =~ /[0-9]+/
|
|
|
|
arg.to_i
|
|
|
|
else
|
|
|
|
RbNaCl::PasswordHash::Argon2.memlimit_value(arg.to_sym)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class StorageKey
|
|
|
|
DEFAULT_OPSLIMIT = RbNaCl::PasswordHash::Argon2.opslimit_value(:interactive)
|
|
|
|
DEFAULT_MEMLIMIT = RbNaCl::PasswordHash::Argon2.memlimit_value(:interactive)
|
|
|
|
SALT_BYTES = RbNaCl::PasswordHash::Argon2::SALTBYTES
|
|
|
|
NONCE_BYTES = RbNaCl::SecretBox::NONCEBYTES
|
|
|
|
DIGEST_BYTES = RbNaCl::SecretBox::KEYBYTES
|
|
|
|
|
|
|
|
attr_accessor :public_key # text (hex encoded)
|
|
|
|
# an ed25519 public key, hex encoded.
|
|
|
|
|
|
|
|
attr_accessor :locked_secretbox # text (hex encoded)
|
|
|
|
# an encrypted Curve25519 private key, hex
|
|
|
|
# encoded. encrypted using the digest of user's
|
|
|
|
# password.
|
|
|
|
|
|
|
|
attr_accessor :sk_nonce # string (hex encoded)
|
|
|
|
# a random nonce used for creating
|
|
|
|
# locked_secretbox
|
|
|
|
|
|
|
|
attr_accessor :pwhash_opslimit # int, in range 3-10
|
|
|
|
attr_accessor :pwhash_memlimit # int, bytes
|
|
|
|
attr_accessor :pwhash_salt # string (hex encoded)
|
|
|
|
|
|
|
|
def generate_new_keypair(password)
|
|
|
|
key = self.new_key()
|
|
|
|
self.encrypt_key(
|
|
|
|
key: key,
|
|
|
|
password: password
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
def to_s
|
|
|
|
attrs = [:public_key, :locked_secretbox, :sk_nonce, :pwhash_opslimit,
|
|
|
|
:pwhash_memlimit, :pwhash_salt]
|
|
|
|
"{\n" + attrs.map{|attr| %( "#{attr}": "#{self.send(attr)}")}.join(",\n") + "\n}\n"
|
|
|
|
end
|
|
|
|
|
|
|
|
protected
|
|
|
|
|
|
|
|
#
|
|
|
|
# given a private key and a password, this will encrypt the key using
|
|
|
|
# the password and save all the necessary values into self.
|
|
|
|
#
|
|
|
|
def encrypt_key(key:, password: nil)
|
|
|
|
unless key.is_a?(RbNaCl::PrivateKey)
|
|
|
|
raise ArgumentError, "key must be an RbNaCl::PrivateKey"
|
|
|
|
end
|
|
|
|
if password.nil? || password.empty?
|
|
|
|
raise ArgumentError, "password is required to encrypt the key"
|
|
|
|
end
|
|
|
|
|
|
|
|
# use KDF to generate a symmetric key from password
|
|
|
|
symmetric_key = password_kdf(password)
|
|
|
|
|
|
|
|
# encrypt the key
|
|
|
|
self.sk_nonce ||= bin2hex(RbNaCl::Random.random_bytes(NONCE_BYTES))
|
|
|
|
secret_box = RbNaCl::SecretBox.new(symmetric_key)
|
|
|
|
encrypted_key = secret_box.encrypt(hex2bin(self.sk_nonce), key.to_bytes)
|
|
|
|
|
|
|
|
# save the key
|
|
|
|
self.public_key = bin2hex(key.public_key.to_bytes)
|
|
|
|
self.locked_secretbox = bin2hex(encrypted_key)
|
|
|
|
end
|
|
|
|
|
|
|
|
#
|
|
|
|
# reverses encrypt_key, returning RbNaCl::PrivateKey
|
|
|
|
#
|
|
|
|
def decrypt_key(password)
|
|
|
|
secret_box = RbNaCl::SecretBox.new(password_kdf(password))
|
|
|
|
return RbNaCl::PrivateKey.new(
|
|
|
|
secret_box.decrypt(
|
|
|
|
hex2bin(self.sk_nonce),
|
|
|
|
hex2bin(self.locked_secretbox)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
rescue RbNaCl::CryptoError
|
|
|
|
raise ArgumentError, 'wrong password'
|
|
|
|
end
|
|
|
|
|
|
|
|
#
|
|
|
|
# generates a new Curve25519 private key
|
|
|
|
#
|
|
|
|
def new_key
|
|
|
|
return RbNaCl::PrivateKey.generate
|
|
|
|
end
|
|
|
|
|
|
|
|
#
|
|
|
|
# argon2 KDF
|
|
|
|
#
|
|
|
|
def password_kdf(secret)
|
|
|
|
self.pwhash_opslimit ||= DEFAULT_OPSLIMIT
|
|
|
|
self.pwhash_memlimit ||= DEFAULT_MEMLIMIT
|
|
|
|
self.pwhash_salt ||= bin2hex(RbNaCl::Random.random_bytes(SALT_BYTES))
|
|
|
|
|
|
|
|
RbNaCl::PasswordHash.argon2(
|
|
|
|
secret,
|
|
|
|
hex2bin(self.pwhash_salt),
|
|
|
|
self.pwhash_opslimit,
|
|
|
|
self.pwhash_memlimit,
|
|
|
|
DIGEST_BYTES
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
def hex2bin(hex)
|
|
|
|
[hex].pack('H*')
|
|
|
|
end
|
|
|
|
|
|
|
|
def bin2hex(binary)
|
|
|
|
binary.unpack('H*').first
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-02-12 19:17:41 +01:00
|
|
|
main()
|