# frozen_string_literal: true # == Schema Information # # Table name: keypairs # # id :bigint(8) not null, primary key # expires_at :datetime # local_fragment :string # private_key :string # public_key :string not null # revoked :boolean default(FALSE), not null # type :integer not null # uri :string # created_at :datetime not null # updated_at :datetime not null # account_id :bigint(8) not null # class Keypair < ApplicationRecord include Expireable self.inheritance_column = nil encrypts :private_key belongs_to :account enum :type, { rsa: 0, ed25519: 1, 'ml-dsa-44': 2, }, validate: true validates :uri, presence: true, uniqueness: true, if: -> { account.remote? } validates :uri, absence: true, if: -> { account.local? } validates :local_fragment, presence: true, uniqueness: { scope: :account_id }, format: { with: /\A#[A-Z0-9-]+\Z/i }, if: -> { account.local? } validates :local_fragment, absence: true, if: -> { account.remote? } validates :public_key, presence: true validates :private_key, presence: true, if: -> { account.local? } validates :private_key, absence: true, if: -> { account.remote? } scope :unexpired, -> { where(expires_at: nil).or(where.not(expires_at: ..Time.now.utc)) } scope :usable, -> { unexpired.where(revoked: false) } alias actor account def full_uri return ActivityPub::TagManager.instance.uri_for(account) + local_fragment if local_fragment.present? uri end def keypair @keypair ||= begin case type when 'rsa' OpenSSL::PKey::RSA.new(private_key || public_key) when 'ed25519', 'ml-dsa-44' OpenSSL::PKey.read(private_key || public_key) end end end def linzer_public_key @linzer_public_key ||= begin case type when 'rsa' Linzer.new_rsa_v1_5_sha256_public_key(public_key) when 'ed25519' Linzer.new_ed25519_public_key(public_key) end end end def linzer_private_key @linzer_private_key ||= begin case type when 'rsa' Linzer.new_rsa_v1_5_sha256_key(private_key, full_uri) when 'ed25519' Linzer.new_ed25519_key(private_key, full_uri) end end end def usable? !revoked? && !expired? end def self.from_keyid(uri) return if uri.blank? keypair = find_by(uri: uri) return keypair unless keypair.nil? # No keypair found, try the old way we used to store RSA keypairs account = ActivityPub::TagManager.instance.uri_to_actor(uri) return if account&.public_key.blank? from_legacy_account(account, uri: uri) end def self.from_legacy_account(account, uri: nil) Keypair.new( account:, uri: uri.presence || ActivityPub::TagManager.instance.key_uri_for(account), public_key: account.public_key, private_key: account.private_key, type: :rsa ) end def self.from_worker_arg(account, private_key_pem_or_hash) if private_key_pem_or_hash.is_a?(String) account.keypairs.build( private_key: private_key_pem_or_hash, local_fragment: '#main-key', type: :rsa ) else account.keypairs.build( private_key: private_key_pem_or_hash['private_key'], public_key: private_key_pem_or_hash['public_key'], uri: private_key_pem_or_hash['uri'], local_fragment: private_key_pem_or_hash['local_fragment'], type: private_key_pem_or_hash['type'] ) end end end