Extract event signing into its own class
This commit is contained in:
parent
97037e6e30
commit
79567fcf22
@ -1,5 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require_relative 'nostr/crypto'
|
||||||
require_relative 'nostr/version'
|
require_relative 'nostr/version'
|
||||||
require_relative 'nostr/keygen'
|
require_relative 'nostr/keygen'
|
||||||
require_relative 'nostr/client_message_type'
|
require_relative 'nostr/client_message_type'
|
||||||
|
48
lib/nostr/crypto.rb
Normal file
48
lib/nostr/crypto.rb
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Nostr
|
||||||
|
# Performs cryptographic operations on a +Nostr::Event+.
|
||||||
|
class Crypto
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# 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
|
@ -70,9 +70,13 @@ module Nostr
|
|||||||
# @example Getting the event id
|
# @example Getting the event id
|
||||||
# event.id # => 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460'
|
# event.id # => 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460'
|
||||||
#
|
#
|
||||||
|
# @example Setting the event id
|
||||||
|
# event.id = 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460'
|
||||||
|
# event.id # => 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460'
|
||||||
|
#
|
||||||
# @return [String|nil]
|
# @return [String|nil]
|
||||||
#
|
#
|
||||||
attr_reader :id
|
attr_accessor :id
|
||||||
|
|
||||||
# 64-bytes signature of the sha256 hash of the serialized event data, which is
|
# 64-bytes signature of the sha256 hash of the serialized event data, which is
|
||||||
# the same as the "id" field
|
# the same as the "id" field
|
||||||
@ -82,9 +86,13 @@ module Nostr
|
|||||||
# @example Getting the event signature
|
# @example Getting the event signature
|
||||||
# event.sig # => ''
|
# event.sig # => ''
|
||||||
#
|
#
|
||||||
|
# @example Setting the event signature
|
||||||
|
# event.sig = '058613f8d34c053294cc28b7f9e1f8f0e80fd1ac94fb20f2da6ca514e7360b39'
|
||||||
|
# event.sig # => '058613f8d34c053294cc28b7f9e1f8f0e80fd1ac94fb20f2da6ca514e7360b39'
|
||||||
|
#
|
||||||
# @return [String|nil]
|
# @return [String|nil]
|
||||||
#
|
#
|
||||||
attr_reader :sig
|
attr_accessor :sig
|
||||||
|
|
||||||
# Instantiates a new Event
|
# Instantiates a new Event
|
||||||
#
|
#
|
||||||
@ -130,6 +138,22 @@ module Nostr
|
|||||||
@content = content
|
@content = content
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Signs an event with the user's private key
|
||||||
|
#
|
||||||
|
# @api public
|
||||||
|
#
|
||||||
|
# @example Signing an event
|
||||||
|
# event.sign(private_key)
|
||||||
|
#
|
||||||
|
# @param private_key [String] 32-bytes hex-encoded private key.
|
||||||
|
#
|
||||||
|
# @return [Event] A signed event.
|
||||||
|
#
|
||||||
|
def sign(private_key)
|
||||||
|
crypto = Crypto.new
|
||||||
|
crypto.sign_event(self, private_key)
|
||||||
|
end
|
||||||
|
|
||||||
# Serializes the event, to obtain a SHA256 digest of it
|
# Serializes the event, to obtain a SHA256 digest of it
|
||||||
#
|
#
|
||||||
# @api public
|
# @api public
|
||||||
|
@ -57,36 +57,8 @@ module Nostr
|
|||||||
# @return [Event]
|
# @return [Event]
|
||||||
#
|
#
|
||||||
def create_event(event_attributes)
|
def create_event(event_attributes)
|
||||||
event_fragment = EventFragment.new(**event_attributes.merge(pubkey: keypair.public_key))
|
event = Event.new(**event_attributes.merge(pubkey: keypair.public_key))
|
||||||
event_sha256 = Digest::SHA256.hexdigest(JSON.dump(event_fragment.serialize))
|
event.sign(keypair.private_key)
|
||||||
|
|
||||||
signature = sign(event_sha256)
|
|
||||||
|
|
||||||
Event.new(
|
|
||||||
id: event_sha256,
|
|
||||||
pubkey: event_fragment.pubkey,
|
|
||||||
created_at: event_fragment.created_at,
|
|
||||||
kind: event_fragment.kind,
|
|
||||||
tags: event_fragment.tags,
|
|
||||||
content: event_fragment.content,
|
|
||||||
sig: signature
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
# Signs an event with the user's private key
|
|
||||||
#
|
|
||||||
# @api private
|
|
||||||
#
|
|
||||||
# @param event_sha256 [String] The SHA256 hash of the event.
|
|
||||||
#
|
|
||||||
# @return [String] The signature of the event.
|
|
||||||
#
|
|
||||||
def sign(event_sha256)
|
|
||||||
hex_private_key = Array(keypair.private_key).pack('H*')
|
|
||||||
hex_message = Array(event_sha256).pack('H*')
|
|
||||||
Schnorr.sign(hex_message, hex_private_key).encode.unpack1('H*')
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
9
sig/nostr/crypto.rbs
Normal file
9
sig/nostr/crypto.rbs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
module Nostr
|
||||||
|
class Crypto
|
||||||
|
def sign_event: (Event, String) -> Event
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def hash_event:(Event) -> String
|
||||||
|
end
|
||||||
|
end
|
@ -5,8 +5,8 @@ module Nostr
|
|||||||
attr_reader kind: Integer
|
attr_reader kind: Integer
|
||||||
attr_reader tags: Array[String]
|
attr_reader tags: Array[String]
|
||||||
attr_reader content: String
|
attr_reader content: String
|
||||||
attr_reader id: String?|nil
|
attr_accessor id: String?|nil
|
||||||
attr_reader sig: String?|nil
|
attr_accessor sig: String?|nil
|
||||||
|
|
||||||
def initialize: (pubkey: String, kind: Integer, content: String, ?created_at: Integer, ?tags: Array[String], ?id: String|nil, ?sig: String|nil) -> void
|
def initialize: (pubkey: String, kind: Integer, content: String, ?created_at: Integer, ?tags: Array[String], ?id: String|nil, ?sig: String|nil) -> void
|
||||||
def serialize: -> [Integer, String, Integer, Integer, Array[String], String]
|
def serialize: -> [Integer, String, Integer, Integer, Array[String], String]
|
||||||
@ -21,5 +21,7 @@ module Nostr
|
|||||||
sig: String?|nil
|
sig: String?|nil
|
||||||
}
|
}
|
||||||
def ==: (Event other) -> bool
|
def ==: (Event other) -> bool
|
||||||
|
|
||||||
|
def sign:(String) -> Event
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
33
spec/nostr/crypto_spec.rb
Normal file
33
spec/nostr/crypto_spec.rb
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe Nostr::Crypto do
|
||||||
|
let(:crypto) { described_class.new }
|
||||||
|
|
||||||
|
describe '#sign_event' do
|
||||||
|
let(:keypair) do
|
||||||
|
Nostr::KeyPair.new(
|
||||||
|
public_key: '8a9d69c56e3c691bec8f9565e4dcbe38ae1d88fffeec3ce66b9f47558a3aa8ca',
|
||||||
|
private_key: '3185a47e3802f956ca5a2b4ea606c1d51c7610f239617e8f0f218d55bdf2b757'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:event) do
|
||||||
|
Nostr::Event.new(
|
||||||
|
kind: Nostr::EventKind::TEXT_NOTE,
|
||||||
|
content: 'Your feedback is appreciated, now pay $8',
|
||||||
|
pubkey: keypair.public_key
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'signs an event' do
|
||||||
|
signed_event = crypto.sign_event(event, keypair.private_key)
|
||||||
|
|
||||||
|
aggregate_failures do
|
||||||
|
expect(signed_event.id.length).to eq(64)
|
||||||
|
expect(signed_event.sig.length).to eq(128)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -121,6 +121,15 @@ RSpec.describe Nostr::Event do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#id=' do
|
||||||
|
it 'sets the event id' do
|
||||||
|
new_id = '20f31a9b2a0ced48a167add9732ccade1dca5e34b44316e37da4af33bc8946a9'
|
||||||
|
|
||||||
|
event.id = new_id
|
||||||
|
expect(event.id).to eq(new_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '#pubkey' do
|
describe '#pubkey' do
|
||||||
it 'exposes the event pubkey' do
|
it 'exposes the event pubkey' do
|
||||||
expect(event.pubkey).to eq('ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460')
|
expect(event.pubkey).to eq('ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460')
|
||||||
@ -156,6 +165,41 @@ RSpec.describe Nostr::Event do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#sig=' do
|
||||||
|
it 'sets the event signature' do
|
||||||
|
new_signature = '058613f8d34c053294cc28b7f9e1f8f0e80fd1ac94fb20f2da6ca514e7360b39' \
|
||||||
|
'63d0086171f842ffebf1f7790ce147b4811a15ef3f59c76ec1324b970cc57ffe'
|
||||||
|
|
||||||
|
event.sig = new_signature
|
||||||
|
expect(event.sig).to eq(new_signature)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#sign' do
|
||||||
|
let(:event) do
|
||||||
|
described_class.new(
|
||||||
|
pubkey: 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460',
|
||||||
|
kind: Nostr::EventKind::TEXT_NOTE,
|
||||||
|
content: 'Your feedback is appreciated, now pay $8'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
let(:keypair) do
|
||||||
|
Nostr::KeyPair.new(
|
||||||
|
public_key: '8a9d69c56e3c691bec8f9565e4dcbe38ae1d88fffeec3ce66b9f47558a3aa8ca',
|
||||||
|
private_key: '3185a47e3802f956ca5a2b4ea606c1d51c7610f239617e8f0f218d55bdf2b757'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'signs the event' do
|
||||||
|
event.sign(keypair.private_key)
|
||||||
|
|
||||||
|
aggregate_failures do
|
||||||
|
expect(event.id.length).to eq(64)
|
||||||
|
expect(event.sig.length).to eq(128)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '#tags' do
|
describe '#tags' do
|
||||||
it 'exposes the event tags' do
|
it 'exposes the event tags' do
|
||||||
expect(event.tags).to eq(
|
expect(event.tags).to eq(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user