trees/bin/trees-create

228 lines
6.1 KiB
Plaintext
Raw Permalink Normal View History

#!/usr/bin/env ruby
#
# This is a simple command line script to create a set of values expected
# by the TREES plugin. Useful for testing.
#
# 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:"
puts " trees-create --password PASSWORD [OPTIONS]"
puts " trees-create --password PASSWORD --old-password PASSWORD OPTIONS"
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"
puts " --secretbox SECRETBOX -- hex encoded secretbox"
puts
puts "for password change all options are required"
exit 1
end
def main
password = nil
st = StorageKey.new
while ARGV.any?
case ARGV.first
when "--old-password"
ARGV.shift
old_password = ARGV.shift
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
when "--secretbox"
ARGV.shift
st.locked_secretbox = ARGV.shift
else
usage
end
end
usage unless password
if old_password.nil?
st.generate_new_keypair(password)
else
st.change_password(old_password, password)
end
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 change_password(old_password, password)
key = self.decrypt_key(old_password)
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
main()