144 lines
4.8 KiB
Ruby
144 lines
4.8 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module Nostr
|
|
# Performs cryptographic operations on a +Nostr::Event+.
|
|
class Crypto
|
|
# Numeric base of the OpenSSL big number used in an event content's encryption.
|
|
#
|
|
# @return [Integer]
|
|
#
|
|
BN_BASE = 16
|
|
|
|
# Name of the cipher curve used in an event content's encryption.
|
|
#
|
|
# @return [String]
|
|
#
|
|
CIPHER_CURVE = 'secp256k1'
|
|
|
|
# Name of the cipher algorithm used in an event content's encryption.
|
|
#
|
|
# @return [String]
|
|
#
|
|
CIPHER_ALGORITHM = 'aes-256-cbc'
|
|
|
|
# Encrypts a piece of text
|
|
#
|
|
# @api public
|
|
#
|
|
# @example Encrypting an event's content
|
|
# crypto = Nostr::Crypto.new
|
|
# encrypted = crypto.encrypt_text(sender_private_key, recipient_public_key, 'Feedback appreciated. Now pay $8')
|
|
# encrypted # => "wrYQaHDfpOEvyJELSCg1vzsywmlJTz8NqH03eFW44s8iQs869jtSb26Lr4s23gmY?iv=v38vAJ3LlJAGZxbmWU4qAg=="
|
|
#
|
|
# @param sender_private_key [String] 32-bytes hex-encoded private key of the creator.
|
|
# @param recipient_public_key [String] 32-bytes hex-encoded public key of the recipient.
|
|
# @param plain_text [String] The text to be encrypted
|
|
#
|
|
# @return [String] Encrypted text.
|
|
#
|
|
def encrypt_text(sender_private_key, recipient_public_key, plain_text)
|
|
cipher = OpenSSL::Cipher.new(CIPHER_ALGORITHM).encrypt
|
|
cipher.iv = iv = cipher.random_iv
|
|
cipher.key = compute_shared_key(sender_private_key, recipient_public_key)
|
|
encrypted_text = cipher.update(plain_text) + cipher.final
|
|
encrypted_text = "#{Base64.encode64(encrypted_text)}?iv=#{Base64.encode64(iv)}"
|
|
encrypted_text.gsub("\n", '')
|
|
end
|
|
|
|
# Decrypts a piece of text
|
|
#
|
|
# @api public
|
|
#
|
|
# @example Encrypting an event's content
|
|
# crypto = Nostr::Crypto.new
|
|
# encrypted # => "wrYQaHDfpOEvyJELSCg1vzsywmlJTz8NqH03eFW44s8iQs869jtSb26Lr4s23gmY?iv=v38vAJ3LlJAGZxbmWU4qAg=="
|
|
# decrypted = crypto.decrypt_text(recipient_private_key, sender_public_key, encrypted)
|
|
#
|
|
# @param sender_public_key [String] 32-bytes hex-encoded public key of the message creator.
|
|
# @param recipient_private_key [String] 32-bytes hex-encoded public key of the recipient.
|
|
# @param encrypted_text [String] The text to be decrypted
|
|
#
|
|
# @return [String] Decrypted text.
|
|
#
|
|
def decrypt_text(recipient_private_key, sender_public_key, encrypted_text)
|
|
base64_encoded_text, iv = encrypted_text.split('?iv=')
|
|
cipher = OpenSSL::Cipher.new(CIPHER_ALGORITHM).decrypt
|
|
cipher.iv = Base64.decode64(iv)
|
|
cipher.key = compute_shared_key(recipient_private_key, sender_public_key)
|
|
plain_text = cipher.update(Base64.decode64(base64_encoded_text)) + cipher.final
|
|
plain_text.force_encoding('UTF-8')
|
|
end
|
|
|
|
# Uses the private key to generate an event id and sign the event
|
|
#
|
|
# @api public
|
|
#
|
|
# @example Signing an event
|
|
# crypto = Nostr::Crypto.new
|
|
# crypto.sign(event, private_key)
|
|
# event.id # => an id
|
|
# event.sig # => a signature
|
|
#
|
|
# @param event [Event] The event to be signed
|
|
# @param private_key [String] 32-bytes hex-encoded private key.
|
|
#
|
|
# @return [Event] An unsigned event.
|
|
#
|
|
def sign_event(event, private_key)
|
|
event_digest = hash_event(event)
|
|
|
|
hex_private_key = Array(private_key).pack('H*')
|
|
hex_message = Array(event_digest).pack('H*')
|
|
event_signature = Schnorr.sign(hex_message, hex_private_key).encode.unpack1('H*')
|
|
|
|
event.id = event_digest
|
|
event.sig = event_signature
|
|
|
|
event
|
|
end
|
|
|
|
private
|
|
|
|
# Finds a shared key between two keys
|
|
#
|
|
# @api private
|
|
#
|
|
# @param private_key [String] 32-bytes hex-encoded private key.
|
|
# @param public_key [String] 32-bytes hex-encoded public key.
|
|
#
|
|
# @return [String] A shared key used in the event's content encryption and decryption.
|
|
#
|
|
def compute_shared_key(private_key, public_key)
|
|
group = OpenSSL::PKey::EC::Group.new(CIPHER_CURVE)
|
|
|
|
private_key_bn = OpenSSL::BN.new(private_key, BN_BASE)
|
|
public_key_bn = OpenSSL::BN.new("02#{public_key}", BN_BASE)
|
|
public_key_point = OpenSSL::PKey::EC::Point.new(group, public_key_bn)
|
|
|
|
asn1 = OpenSSL::ASN1::Sequence(
|
|
[
|
|
OpenSSL::ASN1::Integer.new(1),
|
|
OpenSSL::ASN1::OctetString(private_key_bn.to_s(2)),
|
|
OpenSSL::ASN1::ObjectId(CIPHER_CURVE, 0, :EXPLICIT),
|
|
OpenSSL::ASN1::BitString(public_key_point.to_octet_string(:uncompressed), 1, :EXPLICIT)
|
|
]
|
|
)
|
|
|
|
pkey = OpenSSL::PKey::EC.new(asn1.to_der)
|
|
pkey.dh_compute_key(public_key_point)
|
|
end
|
|
|
|
# Generates a SHA256 hash of a +Nostr::Event+
|
|
#
|
|
# @api private
|
|
#
|
|
# @param event [Event] The event to be hashed
|
|
#
|
|
# @return [String] A SHA256 digest of the event
|
|
#
|
|
def hash_event(event)
|
|
Digest::SHA256.hexdigest(JSON.dump(event.serialize))
|
|
end
|
|
end
|
|
end
|