diff --git a/.env.test b/.env.test index aadec95..fae879b 100644 --- a/.env.test +++ b/.env.test @@ -1,4 +1,5 @@ PRIMARY_DOMAIN=kosmos.org +AKKOUNTS_DOMAIN=accounts.kosmos.org REDIS_URL='redis://localhost:6379/0' diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index a0da71a..4eb4820 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -63,4 +63,9 @@ class ApplicationController < ActionController::Base @fetch_balance_retried = true lndhub_fetch_balance end + + def nostr_event_from_params + params.permit! + params[:signed_event].to_h.symbolize_keys + end end diff --git a/app/controllers/settings_controller.rb b/app/controllers/settings_controller.rb index b736c7f..cb7de77 100644 --- a/app/controllers/settings_controller.rb +++ b/app/controllers/settings_controller.rb @@ -87,25 +87,27 @@ class SettingsController < ApplicationController end def set_nostr_pubkey - signed_event = nostr_event_params[:signed_event].to_h.symbolize_keys + signed_event = Nostr::Event.new(**nostr_event_from_params) - is_valid_id = NostrManager::ValidateId.call(event: signed_event) - is_valid_sig = NostrManager::VerifySignature.call(event: signed_event) - is_correct_content = signed_event[:content] == "Connect my public key to #{current_user.address} (confirmation #{session[:shared_secret]})" + is_valid_sig = signed_event.verify_signature + is_valid_auth = NostrManager::VerifyAuth.call( + event: signed_event, + challenge: session[:shared_secret] + ) - unless is_valid_id && is_valid_sig && is_correct_content + unless is_valid_sig && is_valid_auth flash[:alert] = "Public key could not be verified" http_status :unprocessable_entity and return end - user_with_pubkey = LdapManager::FetchUserByNostrKey.call(pubkey: signed_event[:pubkey]) + user_with_pubkey = LdapManager::FetchUserByNostrKey.call(pubkey: signed_event.pubkey) if user_with_pubkey.present? && (user_with_pubkey != current_user) flash[:alert] = "Public key already in use for a different account" http_status :unprocessable_entity and return end - LdapManager::UpdateNostrKey.call(dn: current_user.dn, pubkey: signed_event[:pubkey]) + LdapManager::UpdateNostrKey.call(dn: current_user.dn, pubkey: signed_event.pubkey) session[:shared_secret] = nil flash[:success] = "Public key verification successful" @@ -160,12 +162,6 @@ class SettingsController < ApplicationController params.require(:user).permit(:current_password) end - def nostr_event_params - params.permit(signed_event: [ - :id, :pubkey, :created_at, :kind, :content, :sig, tags: [] - ]) - end - def generate_email_password characters = [('a'..'z'), ('A'..'Z'), (0..9)].map(&:to_a).flatten SecureRandom.random_bytes(16).each_byte.map { |b| characters[b % characters.length] }.join diff --git a/app/javascript/controllers/settings/nostr_pubkey_controller.js b/app/javascript/controllers/settings/nostr_pubkey_controller.js index 9a3875c..4de30e5 100644 --- a/app/javascript/controllers/settings/nostr_pubkey_controller.js +++ b/app/javascript/controllers/settings/nostr_pubkey_controller.js @@ -3,7 +3,12 @@ import { Controller } from "@hotwired/stimulus" // Connects to data-controller="settings--nostr-pubkey" export default class extends Controller { static targets = [ "noExtension", "setPubkey", "pubkeyBech32Input" ] - static values = { userAddress: String, pubkeyHex: String, sharedSecret: String } + static values = { + userAddress: String, + pubkeyHex: String, + site: String, + sharedSecret: String + } connect () { if (window.nostr) { @@ -19,11 +24,15 @@ export default class extends Controller { this.setPubkeyTarget.disabled = true try { + // Auth based on NIP-42 const signedEvent = await window.nostr.signEvent({ created_at: Math.floor(Date.now() / 1000), - kind: 1, - tags: [], - content: `Connect my public key to ${this.userAddressValue} (confirmation ${this.sharedSecretValue})` + kind: 22242, + tags: [ + ["site", this.siteValue], + ["challenge", this.sharedSecretValue] + ], + content: "" }) const res = await fetch("/settings/set_nostr_pubkey", { diff --git a/app/services/nostr_manager/validate_id.rb b/app/services/nostr_manager/validate_id.rb deleted file mode 100644 index e838441..0000000 --- a/app/services/nostr_manager/validate_id.rb +++ /dev/null @@ -1,11 +0,0 @@ -module NostrManager - class ValidateId < NostrManagerService - def initialize(event:) - @event = Nostr::Event.new(**event) - end - - def call - @event.id == Digest::SHA256.hexdigest(JSON.generate(@event.serialize)) - end - end -end diff --git a/app/services/nostr_manager/verify_auth.rb b/app/services/nostr_manager/verify_auth.rb new file mode 100644 index 0000000..fdcef2d --- /dev/null +++ b/app/services/nostr_manager/verify_auth.rb @@ -0,0 +1,17 @@ +module NostrManager + class VerifyAuth < NostrManagerService + def initialize(event:, challenge:) + @event = event + @challenge_expected = challenge + @site_expected = Setting.accounts_domain + end + + def call + site_given = @event.tags.find{|t| t[0] == "site"}[1] + challenge_given = @event.tags.find{|t| t[0] == "challenge"}[1] + + site_given == @site_expected && + challenge_given == @challenge_expected + end + end +end diff --git a/app/services/nostr_manager/verify_signature.rb b/app/services/nostr_manager/verify_signature.rb deleted file mode 100644 index 9c62234..0000000 --- a/app/services/nostr_manager/verify_signature.rb +++ /dev/null @@ -1,17 +0,0 @@ -module NostrManager - class VerifySignature < NostrManagerService - def initialize(event:) - @event = Nostr::Event.new(**event) - end - - def call - Schnorr.check_sig!( - [@event.id].pack('H*'), - [@event.pubkey].pack('H*'), - [@event.sig].pack('H*') - ) - rescue Schnorr::InvalidSignatureError - false - end - end -end diff --git a/app/views/settings/_nostr.html.erb b/app/views/settings/_nostr.html.erb index 3c1a5b2..1251dd5 100644 --- a/app/views/settings/_nostr.html.erb +++ b/app/views/settings/_nostr.html.erb @@ -3,6 +3,7 @@