lib/slosilo/key.rb in slosilo-0.0.0 vs lib/slosilo/key.rb in slosilo-0.1.2
- old
+ new
@@ -1,26 +1,20 @@
require 'openssl'
require 'json'
require 'base64'
require 'time'
-require 'slosilo/errors'
-
module Slosilo
class Key
def initialize raw_key = nil
@key = if raw_key.is_a? OpenSSL::PKey::RSA
raw_key
elsif !raw_key.nil?
OpenSSL::PKey.read raw_key
else
OpenSSL::PKey::RSA.new 2048
end
- rescue OpenSSL::PKey::PKeyError => e
- # old openssl versions used to report ArgumentError
- # which arguably makes more sense here, so reraise as that
- raise ArgumentError, e, e.backtrace
end
attr_reader :key
def cipher
@@ -31,32 +25,22 @@
key = cipher.random_key
ctxt = cipher.encrypt plaintext, key: key
key = @key.public_encrypt key
[ctxt, key]
end
-
- def encrypt_message plaintext
- c, k = encrypt plaintext
- k + c
- end
def decrypt ciphertext, skey
key = @key.private_decrypt skey
cipher.decrypt ciphertext, key: key
end
-
- def decrypt_message ciphertext
- k, c = ciphertext.unpack("A256A*")
- decrypt c, k
- end
def to_s
@key.public_key.to_pem
end
def to_der
- @to_der ||= @key.to_der
+ @key.to_der
end
def sign value
sign_string(stringify value)
end
@@ -72,143 +56,36 @@
# create a new timestamped and signed token carrying data
def signed_token data
token = { "data" => data, "timestamp" => Time.new.utc.to_s }
token["signature"] = Base64::urlsafe_encode64(sign token)
- token["key"] = fingerprint
token
end
-
- JWT_ALGORITHM = 'conjur.org/slosilo/v2'.freeze
-
- # Issue a JWT with the given claims.
- # `iat` (issued at) claim is automatically added.
- # Other interesting claims you can give are:
- # - `sub` - token subject, for example a user name;
- # - `exp` - expiration time (absolute);
- # - `cidr` (Conjur extension) - array of CIDR masks that are accepted to
- # make requests that bear this token
- def issue_jwt claims
- token = Slosilo::JWT.new claims
- token.add_signature \
- alg: JWT_ALGORITHM,
- kid: fingerprint,
- &method(:sign)
- token.freeze
- end
-
- DEFAULT_EXPIRATION = 8 * 60
- def token_valid? token, expiry = DEFAULT_EXPIRATION
- return jwt_valid? token if token.respond_to? :header
+ def token_valid? token, expiry = 8 * 60
token = token.clone
- expected_key = token.delete "key"
- return false if (expected_key and (expected_key != fingerprint))
signature = Base64::urlsafe_decode64(token.delete "signature")
(Time.parse(token["timestamp"]) + expiry > Time.now) && verify_signature(token, signature)
end
-
- # Validate a JWT.
- #
- # Convenience method calling #validate_jwt and returning false if an
- # exception is raised.
- #
- # @param token [JWT] pre-parsed token to verify
- # @return [Boolean]
- def jwt_valid? token
- validate_jwt token
- true
- rescue
- false
- end
-
- # Validate a JWT.
- #
- # First checks whether algorithm is 'conjur.org/slosilo/v2' and the key id
- # matches this key's fingerprint. Then verifies if the token is not expired,
- # as indicated by the `exp` claim; in its absence tokens are assumed to
- # expire in `iat` + 8 minutes.
- #
- # If those checks pass, finally the signature is verified.
- #
- # @raises TokenValidationError if any of the checks fail.
- #
- # @note It's the responsibility of the caller to examine other claims
- # included in the token; consideration needs to be given to handling
- # unrecognized claims.
- #
- # @param token [JWT] pre-parsed token to verify
- def validate_jwt token
- def err msg
- raise Error::TokenValidationError, msg, caller
- end
-
- header = token.header
- err 'unrecognized algorithm' unless header['alg'] == JWT_ALGORITHM
- err 'mismatched key' if (kid = header['kid']) && kid != fingerprint
- iat = Time.at token.claims['iat'] || err('unknown issuing time')
- exp = Time.at token.claims['exp'] || (iat + DEFAULT_EXPIRATION)
- err 'token expired' if exp <= Time.now
- err 'invalid signature' unless verify_signature token.string_to_sign, token.signature
- true
- end
def sign_string value
- salt = shake_salt
- key.private_encrypt(hash_function.digest(salt + value)) + salt
+ _salt = salt
+ key.private_encrypt(hash_function.digest(_salt + value)) + _salt
end
- def fingerprint
- @fingerprint ||= OpenSSL::Digest::SHA256.hexdigest key.public_key.to_der
- end
-
- def == other
- to_der == other.to_der
- end
-
- alias_method :eql?, :==
-
- def hash
- to_der.hash
- end
-
- # return a new key with just the public part of this
- def public
- Key.new(@key.public_key)
- end
-
- # checks if the keypair contains a private key
- def private?
- @key.private?
- end
-
private
-
- # Note that this is currently somewhat shallow stringification --
- # to implement originating tokens we may need to make it deeper.
def stringify value
- string = case value
+ case value
when Hash
value.to_a.sort.to_json
when String
value
else
value.to_json
end
-
- # Make sure that the string is ascii_8bit (i.e. raw bytes), and represents
- # the utf-8 encoding of the string. This accomplishes two things: it normalizes
- # the representation of the string at the byte level (so we don't have an error if
- # one username is submitted as ISO-whatever, and the next as UTF-16), and it prevents
- # an incompatible encoding error when we concatenate it with the salt.
- if string.encoding != Encoding::ASCII_8BIT
- string.encode(Encoding::UTF_8).force_encoding(Encoding::ASCII_8BIT)
- else
- string
- end
end
- def shake_salt
+ def salt
Slosilo::Random::salt
end
def hash_function
@hash_function ||= OpenSSL::Digest::SHA256