Implement NIP-19 bech32-encoded private and public keys
https://github.com/nostr-protocol/nips/blob/master/19.md
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'nostr/errors'
|
||||
require_relative 'nostr/crypto'
|
||||
require_relative 'nostr/version'
|
||||
require_relative 'nostr/keygen'
|
||||
@@ -14,6 +15,9 @@ require_relative 'nostr/event'
|
||||
require_relative 'nostr/events/encrypted_direct_message'
|
||||
require_relative 'nostr/client'
|
||||
require_relative 'nostr/user'
|
||||
require_relative 'nostr/key'
|
||||
require_relative 'nostr/private_key'
|
||||
require_relative 'nostr/public_key'
|
||||
|
||||
# Encapsulates all the gem's logic
|
||||
module Nostr
|
||||
|
||||
@@ -30,8 +30,8 @@ module Nostr
|
||||
# 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 sender_private_key [PrivateKey] 32-bytes hex-encoded private key of the creator.
|
||||
# @param recipient_public_key [PublicKey] 32-bytes hex-encoded public key of the recipient.
|
||||
# @param plain_text [String] The text to be encrypted
|
||||
#
|
||||
# @return [String] Encrypted text.
|
||||
@@ -54,8 +54,8 @@ module Nostr
|
||||
# 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 sender_public_key [PublicKey] 32-bytes hex-encoded public key of the message creator.
|
||||
# @param recipient_private_key [PrivateKey] 32-bytes hex-encoded public key of the recipient.
|
||||
# @param encrypted_text [String] The text to be decrypted
|
||||
#
|
||||
# @return [String] Decrypted text.
|
||||
@@ -84,7 +84,7 @@ module Nostr
|
||||
# event.sig # => a signature
|
||||
#
|
||||
# @param event [Event] The event to be signed
|
||||
# @param private_key [String] 32-bytes hex-encoded private key.
|
||||
# @param private_key [PrivateKey] 32-bytes hex-encoded private key.
|
||||
#
|
||||
# @return [Event] An unsigned event.
|
||||
#
|
||||
@@ -107,8 +107,8 @@ module Nostr
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @param private_key [String] 32-bytes hex-encoded private key.
|
||||
# @param public_key [String] 32-bytes hex-encoded public key.
|
||||
# @param private_key [PrivateKey] 32-bytes hex-encoded private key.
|
||||
# @param public_key [PublicKey] 32-bytes hex-encoded public key.
|
||||
#
|
||||
# @return [String] A shared key used in the event's content encryption and decryption.
|
||||
#
|
||||
|
||||
8
lib/nostr/errors.rb
Normal file
8
lib/nostr/errors.rb
Normal file
@@ -0,0 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'errors/error'
|
||||
require_relative 'errors/key_validation_error'
|
||||
require_relative 'errors/invalid_hrp_error'
|
||||
require_relative 'errors/invalid_key_type_error'
|
||||
require_relative 'errors/invalid_key_length_error'
|
||||
require_relative 'errors/invalid_key_format_error'
|
||||
7
lib/nostr/errors/error.rb
Normal file
7
lib/nostr/errors/error.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Nostr
|
||||
# Base error class
|
||||
class Error < StandardError
|
||||
end
|
||||
end
|
||||
21
lib/nostr/errors/invalid_hrp_error.rb
Normal file
21
lib/nostr/errors/invalid_hrp_error.rb
Normal file
@@ -0,0 +1,21 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Nostr
|
||||
# Raised when the human readable part of a Bech32 string is invalid
|
||||
#
|
||||
# @api public
|
||||
#
|
||||
class InvalidHRPError < KeyValidationError
|
||||
# Initializes the error
|
||||
#
|
||||
# @example
|
||||
# InvalidHRPError.new('example wrong hrp', 'nsec')
|
||||
#
|
||||
# @param given_hrp [String] The given human readable part of the Bech32 string
|
||||
# @param allowed_hrp [String] The allowed human readable part of the Bech32 string
|
||||
#
|
||||
def initialize(given_hrp, allowed_hrp)
|
||||
super("Invalid hrp: #{given_hrp}. The allowed hrp value for this kind of entity is '#{allowed_hrp}'.")
|
||||
end
|
||||
end
|
||||
end
|
||||
20
lib/nostr/errors/invalid_key_format_error.rb
Normal file
20
lib/nostr/errors/invalid_key_format_error.rb
Normal file
@@ -0,0 +1,20 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Nostr
|
||||
# Raised when the private key is in an invalid format
|
||||
#
|
||||
# @api public
|
||||
#
|
||||
class InvalidKeyFormatError < KeyValidationError
|
||||
# Initializes the error
|
||||
#
|
||||
# @example
|
||||
# InvalidKeyFormatError.new('private'')
|
||||
#
|
||||
# @param [String] key_kind The kind of key that is invalid (public or private)
|
||||
#
|
||||
def initialize(key_kind)
|
||||
super("Only lowercase hexadecimal characters are allowed in #{key_kind} keys.")
|
||||
end
|
||||
end
|
||||
end
|
||||
20
lib/nostr/errors/invalid_key_length_error.rb
Normal file
20
lib/nostr/errors/invalid_key_length_error.rb
Normal file
@@ -0,0 +1,20 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Nostr
|
||||
# Raised when the private key's length is not 64 characters
|
||||
#
|
||||
# @api public
|
||||
#
|
||||
class InvalidKeyLengthError < KeyValidationError
|
||||
# Initializes the error
|
||||
#
|
||||
# @example
|
||||
# InvalidKeyLengthError.new('private'')
|
||||
#
|
||||
# @param [String] key_kind The kind of key that is invalid (public or private)
|
||||
#
|
||||
def initialize(key_kind)
|
||||
super("Invalid #{key_kind} key length. It should have 64 characters.")
|
||||
end
|
||||
end
|
||||
end
|
||||
18
lib/nostr/errors/invalid_key_type_error.rb
Normal file
18
lib/nostr/errors/invalid_key_type_error.rb
Normal file
@@ -0,0 +1,18 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Nostr
|
||||
# Raised when the private key is not a string
|
||||
#
|
||||
# @api public
|
||||
#
|
||||
class InvalidKeyTypeError < KeyValidationError
|
||||
# Initializes the error
|
||||
#
|
||||
# @example
|
||||
# InvalidKeyTypeError.new('private'')
|
||||
#
|
||||
# @param [String] key_kind The kind of key that is invalid (public or private)
|
||||
#
|
||||
def initialize(key_kind) = super("Invalid #{key_kind} key type")
|
||||
end
|
||||
end
|
||||
6
lib/nostr/errors/key_validation_error.rb
Normal file
6
lib/nostr/errors/key_validation_error.rb
Normal file
@@ -0,0 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Nostr
|
||||
# Base class for all key validation errors
|
||||
class KeyValidationError < Error; end
|
||||
end
|
||||
@@ -159,11 +159,11 @@ module Nostr
|
||||
# pubkey = '48df4af6e240ac5f7c5de89bf5941b249880be0e87d03685b178ccb1a315f52e'
|
||||
# event.add_pubkey_reference(pubkey)
|
||||
#
|
||||
# @param pubkey [String] 32-bytes hex-encoded public key.
|
||||
# @param pubkey [PublicKey] 32-bytes hex-encoded public key.
|
||||
#
|
||||
# @return [Array<String>] The event's updated list of tags
|
||||
#
|
||||
def add_pubkey_reference(pubkey) = tags.push(['p', pubkey])
|
||||
def add_pubkey_reference(pubkey) = tags.push(['p', pubkey.to_s])
|
||||
|
||||
# Signs an event with the user's private key
|
||||
#
|
||||
@@ -172,7 +172,7 @@ module Nostr
|
||||
# @example Signing an event
|
||||
# event.sign(private_key)
|
||||
#
|
||||
# @param private_key [String] 32-bytes hex-encoded private key.
|
||||
# @param private_key [PrivateKey] 32-bytes hex-encoded private key.
|
||||
#
|
||||
# @return [Event] A signed event.
|
||||
#
|
||||
|
||||
@@ -26,8 +26,9 @@ module Nostr
|
||||
# )
|
||||
#
|
||||
# @param plain_text [String] The +content+ of the encrypted message.
|
||||
# @param sender_private_key [String] 32-bytes hex-encoded private key of the message's author.
|
||||
# @param recipient_public_key [String] 32-bytes hex-encoded public key of the recipient of the encrypted message.
|
||||
# @param sender_private_key [PrivateKey] 32-bytes hex-encoded private key of the message's author.
|
||||
# @param recipient_public_key [PublicKey] 32-bytes hex-encoded public key of the recipient of the encrypted
|
||||
# message.
|
||||
# @param previous_direct_message [String] 32-bytes hex-encoded id identifying the previous message in a
|
||||
# conversation or a message we are explicitly replying to (such that contextual, more organized conversations
|
||||
# may happen
|
||||
|
||||
102
lib/nostr/key.rb
Normal file
102
lib/nostr/key.rb
Normal file
@@ -0,0 +1,102 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'bech32'
|
||||
|
||||
module Nostr
|
||||
# Abstract class for all keys
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
class Key < String
|
||||
# The regular expression for hexadecimal lowercase characters
|
||||
#
|
||||
# @return [Regexp] The regular expression for hexadecimal lowercase characters
|
||||
#
|
||||
FORMAT = /^[a-f0-9]+$/
|
||||
|
||||
# The length of the key in hex format
|
||||
#
|
||||
# @return [Integer] The length of the key in hex format
|
||||
#
|
||||
LENGTH = 64
|
||||
|
||||
# Instantiates a new key. Can't be used directly because this is an abstract class. Raises a +ValidationError+
|
||||
#
|
||||
# @see Nostr::PrivateKey
|
||||
# @see Nostr::PublicKey
|
||||
#
|
||||
# @param [String] hex_value Hex-encoded value of the key
|
||||
#
|
||||
# @raise [ValidationError]
|
||||
#
|
||||
def initialize(hex_value)
|
||||
validate_hex_value(hex_value)
|
||||
|
||||
super(hex_value)
|
||||
end
|
||||
|
||||
# Instantiates a key from a bech32 string
|
||||
#
|
||||
# @api public
|
||||
#
|
||||
# @example
|
||||
# bech32_key = 'nsec1vl029mgpspedva04g90vltkh6fvh240zqtv9k0t9af8935ke9laqsnlfe5'
|
||||
# bech32_key.to_key # => #<Nostr::PublicKey:0x000000010601e3c8 @hex_value="...">
|
||||
#
|
||||
# @raise [Nostr::InvalidHRPError] if the bech32 string is invalid.
|
||||
#
|
||||
# @param [String] bech32_value The bech32 string representation of the key.
|
||||
#
|
||||
# @return [Key] the key.
|
||||
#
|
||||
def self.from_bech32(bech32_value)
|
||||
entity = Bech32::Nostr::NIP19.decode(bech32_value)
|
||||
|
||||
raise InvalidHRPError.new(entity.hrp, hrp) unless entity.hrp == hrp
|
||||
|
||||
new(entity.data)
|
||||
end
|
||||
|
||||
# Abstract method to be implemented by subclasses to provide the HRP (npub, nsec)
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @return [String] The HRP
|
||||
#
|
||||
def self.hrp
|
||||
raise 'Subclasses must implement this method'
|
||||
end
|
||||
|
||||
# Converts the key to a bech32 string representation
|
||||
#
|
||||
# @api public
|
||||
#
|
||||
# @example Converting a private key to a bech32 string
|
||||
# public_key = Nostr::PrivateKey.new('67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa')
|
||||
# public_key.to_bech32 # => 'nsec1vl029mgpspedva04g90vltkh6fvh240zqtv9k0t9af8935ke9laqsnlfe5'
|
||||
#
|
||||
# @example Converting a public key to a bech32 string
|
||||
# public_key = Nostr::PublicKey.new('7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e')
|
||||
# public_key.to_bech32 # => 'npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg'
|
||||
#
|
||||
# @return [String] The bech32 string representation of the key
|
||||
#
|
||||
def to_bech32 = Bech32::Nostr::BareEntity.new(self.class.hrp, self).encode
|
||||
|
||||
protected
|
||||
|
||||
# Validates the hex value during initialization
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @param [String] _hex_value The hex value of the key
|
||||
#
|
||||
# @raise [KeyValidationError] When the hex value is invalid
|
||||
#
|
||||
# @return [void]
|
||||
#
|
||||
def validate_hex_value(_hex_value)
|
||||
raise 'Subclasses must implement this method'
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -10,7 +10,7 @@ module Nostr
|
||||
# @example
|
||||
# keypair.private_key # => '893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900'
|
||||
#
|
||||
# @return [String]
|
||||
# @return [PrivateKey]
|
||||
#
|
||||
attr_reader :private_key
|
||||
|
||||
@@ -21,7 +21,7 @@ module Nostr
|
||||
# @example
|
||||
# keypair.public_key # => '2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'
|
||||
#
|
||||
# @return [String]
|
||||
# @return [PublicKey]
|
||||
#
|
||||
attr_reader :public_key
|
||||
|
||||
@@ -31,16 +31,40 @@ module Nostr
|
||||
#
|
||||
# @example
|
||||
# keypair = Nostr::KeyPair.new(
|
||||
# private_key: '893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900',
|
||||
# public_key: '2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558',
|
||||
# private_key: Nostr::PrivateKey.new('893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900'),
|
||||
# public_key: Nostr::PublicKey.new('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'),
|
||||
# )
|
||||
#
|
||||
# @param private_key [String] 32-bytes hex-encoded private key.
|
||||
# @param public_key [String] 32-bytes hex-encoded public key.
|
||||
# @param private_key [PrivateKey] 32-bytes hex-encoded private key.
|
||||
# @param public_key [PublicKey] 32-bytes hex-encoded public key.
|
||||
#
|
||||
# @raise ArgumentError when the private key is not a +PrivateKey+
|
||||
# @raise ArgumentError when the public key is not a +PublicKey+
|
||||
#
|
||||
def initialize(private_key:, public_key:)
|
||||
validate_keys(private_key, public_key)
|
||||
|
||||
@private_key = private_key
|
||||
@public_key = public_key
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Validates the keys
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @param private_key [PrivateKey] 32-bytes hex-encoded private key.
|
||||
# @param public_key [PublicKey] 32-bytes hex-encoded public key.
|
||||
#
|
||||
# @raise ArgumentError when the private key is not a +PrivateKey+
|
||||
# @raise ArgumentError when the public key is not a +PublicKey+
|
||||
#
|
||||
# @return [void]
|
||||
#
|
||||
def validate_keys(private_key, public_key)
|
||||
raise ArgumentError, 'private_key is not an instance of PrivateKey' unless private_key.is_a?(Nostr::PrivateKey)
|
||||
raise ArgumentError, 'public_key is not an instance of PublicKey' unless public_key.is_a?(Nostr::PublicKey)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -44,10 +44,11 @@ module Nostr
|
||||
# private_key = keygen.generate_private_key
|
||||
# private_key # => '893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900'
|
||||
#
|
||||
# @return [String] A 32-bytes hex-encoded private key.
|
||||
# @return [PrivateKey] A 32-bytes hex-encoded private key.
|
||||
#
|
||||
def generate_private_key
|
||||
(SecureRandom.random_number(group.order - 1) + 1).to_s(16)
|
||||
hex_value = (SecureRandom.random_number(group.order - 1) + 1).to_s(16)
|
||||
PrivateKey.new(hex_value)
|
||||
end
|
||||
|
||||
# Extracts a public key from a private key
|
||||
@@ -59,10 +60,36 @@ module Nostr
|
||||
# public_key = keygen.extract_public_key(private_key)
|
||||
# public_key # => '2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'
|
||||
#
|
||||
# @return [String] A 32-bytes hex-encoded public key.
|
||||
# @param [PrivateKey] private_key A 32-bytes hex-encoded private key.
|
||||
#
|
||||
# @raise [ArgumentError] if the private key is not an instance of +PrivateKey+
|
||||
#
|
||||
# @return [PublicKey] A 32-bytes hex-encoded public key.
|
||||
#
|
||||
def extract_public_key(private_key)
|
||||
group.generator.multiply_by_scalar(private_key.to_i(16)).x.to_s(16).rjust(64, '0')
|
||||
validate_private_key(private_key)
|
||||
hex_value = group.generator.multiply_by_scalar(private_key.to_i(16)).x.to_s(16).rjust(64, '0')
|
||||
PublicKey.new(hex_value)
|
||||
end
|
||||
|
||||
# Builds a key pair from an existing private key
|
||||
#
|
||||
# @api public
|
||||
#
|
||||
# @example
|
||||
# private_key = Nostr::PrivateKey.new('893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900')
|
||||
# keygen.get_key_pair_from_private_key(private_key)
|
||||
#
|
||||
# @param private_key [PrivateKey] 32-bytes hex-encoded private key.
|
||||
#
|
||||
# @raise [ArgumentError] if the private key is not an instance of +PrivateKey+
|
||||
#
|
||||
# @return [Nostr::KeyPair]
|
||||
#
|
||||
def get_key_pair_from_private_key(private_key)
|
||||
validate_private_key(private_key)
|
||||
public_key = extract_public_key(private_key)
|
||||
KeyPair.new(private_key:, public_key:)
|
||||
end
|
||||
|
||||
private
|
||||
@@ -74,5 +101,17 @@ module Nostr
|
||||
# @return [ECDSA::Group]
|
||||
#
|
||||
attr_reader :group
|
||||
|
||||
# Validates that the private key is an instance of +PrivateKey+
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @raise [ArgumentError] if the private key is not an instance of +PrivateKey+
|
||||
#
|
||||
# @return [void]
|
||||
#
|
||||
def validate_private_key(private_key)
|
||||
raise ArgumentError, 'private_key is not an instance of PrivateKey' unless private_key.is_a?(Nostr::PrivateKey)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
36
lib/nostr/private_key.rb
Normal file
36
lib/nostr/private_key.rb
Normal file
@@ -0,0 +1,36 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Nostr
|
||||
# 32-bytes lowercase hex-encoded private key
|
||||
class PrivateKey < Key
|
||||
# Human-readable part of the Bech32 encoded address
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @return [String] The human-readable part of the Bech32 encoded address
|
||||
#
|
||||
def self.hrp
|
||||
'nsec'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Validates the hex value of the private key
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @param [String] hex_value The private key in hex format
|
||||
#
|
||||
# @raise InvalidKeyTypeError when the private key is not a string
|
||||
# @raise InvalidKeyLengthError when the private key's length is not 64 characters
|
||||
# @raise InvalidKeyFormatError when the private key is in an invalid format
|
||||
#
|
||||
# @return [void]
|
||||
#
|
||||
def validate_hex_value(hex_value)
|
||||
raise InvalidKeyTypeError, 'private' unless hex_value.is_a?(String)
|
||||
raise InvalidKeyLengthError, 'private' unless hex_value.size == Key::LENGTH
|
||||
raise InvalidKeyFormatError, 'private' unless hex_value.match(Key::FORMAT)
|
||||
end
|
||||
end
|
||||
end
|
||||
36
lib/nostr/public_key.rb
Normal file
36
lib/nostr/public_key.rb
Normal file
@@ -0,0 +1,36 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Nostr
|
||||
# 32-bytes lowercase hex-encoded public key
|
||||
class PublicKey < Key
|
||||
# Human-readable part of the Bech32 encoded address
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @return [String] The human-readable part of the Bech32 encoded address
|
||||
#
|
||||
def self.hrp
|
||||
'npub'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Validates the hex value of the public key
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @param [String] hex_value The public key in hex format
|
||||
#
|
||||
# @raise InvalidKeyTypeError when the public key is not a string
|
||||
# @raise InvalidKeyLengthError when the public key's length is not 64 characters
|
||||
# @raise InvalidKeyFormatError when the public key is in an invalid format
|
||||
#
|
||||
# @return [void]
|
||||
#
|
||||
def validate_hex_value(hex_value)
|
||||
raise InvalidKeyTypeError, 'public' unless hex_value.is_a?(String)
|
||||
raise InvalidKeyLengthError, 'public' unless hex_value.size == Key::LENGTH
|
||||
raise InvalidKeyFormatError, 'public' unless hex_value.match(Key::FORMAT)
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user