Skip to content

Instantly share code, notes, and snippets.

@stevepolitodesign
Last active April 15, 2022 21:59
Show Gist options
  • Save stevepolitodesign/622c79381afec43a050757d0a425f77a to your computer and use it in GitHub Desktop.
Save stevepolitodesign/622c79381afec43a050757d0a425f77a to your computer and use it in GitHub Desktop.
Send secure messages in Rails. Work in progress.
# app/controllers/secret_messages_controller.rb
class SecretMessagesController < ApplicationController
  def create
    @secret_message_form = SecretMessageForm.new(secret_message_form_params)

    if @secret_message_form.valid?
      encrypted_content = encrypt_content(
        seed_phrase: params[:secret_message_form][:seed_phrase],
        password: params[:secret_message_form][:password],
        content: @secret_message_form.content
      )
      secret_message = SecretMessage.new(
        content: encrypted_content,
        expires_at: @secret_message_form.expires_at
      )

      redirect_to secret_message_path(
        verifier.generate(
          secret_message,
          expires_at: secret_message.expires_at,
          purpose: :view_secret_message
        )
      )
    else
      render :new, status: :unprocessable_entity
    end
  end

  def decrypt
    @secret_message = verifier.verify(params[:signed_message], purpose: :view_secret_message)

    decrypted_content = decrypt_content(
      seed_phrase: params[:secret_message][:seed_phrase],
      password: params[:secret_message][:password],
      content: @secret_message.content
    )

    render plain: decrypted_content, status: :ok
  rescue ActiveSupport::MessageEncryptor::InvalidMessage
    redirect_to secret_message_path(params[:signed_message]), alert: "Incorrect password or seed phrase"
  end

  def new
    @secret_message_form = SecretMessageForm.new
  end

  def show
    @secret_message = verifier.verify(params[:signed_message], purpose: :view_secret_message)
  rescue ActiveSupport::MessageVerifier::InvalidSignature
    render plain: "Invalid URL", status: 404
  end

  private

  def crypt(seed_phrase:, password:)
    len = Rails.application.credentials.message_encryptor_length!
    key = ActiveSupport::KeyGenerator.new(password).generate_key(seed_phrase, len)
    ActiveSupport::MessageEncryptor.new(key)
  end

  def decrypt_content(seed_phrase:, password:, content:)
    crypt(seed_phrase: seed_phrase, password: password).decrypt_and_verify(content)
  end

  def encrypt_content(seed_phrase:, password:, content:)
    crypt(seed_phrase: seed_phrase, password: password).encrypt_and_sign(content)
  end

  def secret_message_form_params
    params.require(:secret_message_form)
      .permit(
        :content,
        :expires_at,
        :seed_phrase,
        :seed_phrase_confirmation,
        :password,
        :password_confirmation
      )
  end

  def verifier
    ActiveSupport::MessageVerifier.new Rails.application.credentials.message_verifier_secret!
  end
end
# app/models/secret_message_form.rb
class SecretMessageForm
  include ActiveModel::Model
  include ActiveModel::Attributes
  include ActiveModel::Validations

  attribute :content, :string
  attribute :expires_at, :datetime
  attribute :seed_phrase, :string
  attribute :password, :string

  validates :content, :expires_at, :seed_phrase, :seed_phrase_confirmation, :password, :password_confirmation, presence: true
  validates :seed_phrase, :password, confirmation: true
  validate :expires_at_must_be_in_the_future, if: :expires_at_is_present?

  private

  def expires_at_must_be_in_the_future
    if expires_at <= Time.current
      errors.add(:expires_at, "must by in the future")
    end
  end

  def expires_at_is_present?
    expires_at.presence
  end
end
# app/models/secret_message.rb
class SecretMessage
  attr_accessor :content, :expires_at

  def initialize(content:, expires_at:)
    @content = content
    @expires_at = expires_at
  end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment