Class: Terminalwire::V2::Rails::Session

Inherits:
Object
  • Object
show all
Extended by:
Forwardable
Defined in:
lib/terminalwire/v2/rails.rb

Overview

A session stored on the CLIENT — the v2-native version of v1's Terminalwire::Rails::Session, so a v2-only app needs no v1 gem. It reads/writes via the context (file/directory/storage_path, which v2 implements identically to v1).

The payload is ENCRYPTED and signed (ActiveSupport::MessageEncryptor, AES-256-GCM — the same primitive behind Rails' encrypted cookies), so the contents are confidential on the client machine, not just tamper-proof. The key is derived from the app's secret_key_base with a Terminalwire-specific salt, so it is never the raw secret and never shared with any other verifier/encryptor in the app. Sessions expire (default 30 days, clock reset on every write); an expired session reads as empty.

Earlier releases stored the session as an HS256 JWT — signed but NOT encrypted, despite docs saying otherwise. Those legacy sessions are read one last time and immediately rewritten encrypted, so upgrading logs nobody out.

Resilient by design: a missing, empty, tampered, expired, or wrong-key session reads as EMPTY, so the user simply logs in again — upgrading (or rotating the secret) never crashes a command, it just signs them out.

Constant Summary collapse

FILENAME =

Kept from the JWT era so upgraded clients keep using the same file.

"session.jwt"
EMPTY_SESSION =
{}.freeze
KEY_SALT =

Key-derivation salt — Terminalwire-specific so the derived key can't collide with any key the app derives from secret_key_base elsewhere.

"terminalwire session"
CIPHER =

AEAD cipher: encrypts AND authenticates in one primitive (the same one behind Rails' encrypted cookies). Pinned explicitly, not left to the app's ActiveSupport default (which is aes-256-cbc unless configured otherwise).

"aes-256-gcm"
DEFAULT_EXPIRES_IN =

30 days, refreshed on every write

60 * 60 * 24 * 30

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(context:, path: nil, secret_key: self.class.secret_key, expires_in: DEFAULT_EXPIRES_IN) ⇒ Session

Returns a new instance of Session.



94
95
96
97
98
99
100
101
# File 'lib/terminalwire/v2/rails.rb', line 94

def initialize(context:, path: nil, secret_key: self.class.secret_key, expires_in: DEFAULT_EXPIRES_IN)
  @context = context
  @path = Pathname.new(path || context.storage_path)
  @config_file_path = @path.join(FILENAME)
  @secret_key = secret_key
  @expires_in = expires_in
  ensure_file
end

Class Method Details

.secret_keyObject



138
139
140
# File 'lib/terminalwire/v2/rails.rb', line 138

def self.secret_key
  ::Rails.application.secret_key_base
end

Instance Method Details

#[]=(key, value) ⇒ Object



129
130
131
# File 'lib/terminalwire/v2/rails.rb', line 129

def []=(key, value)
  edit { |config| config[key] = value }
end

#edit {|config| ... } ⇒ Object

Yields:

  • (config)


123
124
125
126
127
# File 'lib/terminalwire/v2/rails.rb', line 123

def edit
  config = read.dup
  yield config
  write(config)
end

#readObject

The session payload, or EMPTY_SESSION when there isn't a valid one (missing, empty, tampered, expired, wrong key, unreadable). To the user these all mean the same thing — log in again — so none of them raise.



106
107
108
109
110
111
112
113
114
115
# File 'lib/terminalwire/v2/rails.rb', line 106

def read
  token = @context.file.read(@config_file_path)
  return EMPTY_SESSION if token.nil? || token.to_s.empty?

  encryptor.decrypt_and_verify(token) || EMPTY_SESSION
rescue ActiveSupport::MessageEncryptor::InvalidMessage
  read_and_migrate_legacy(token)
rescue StandardError
  EMPTY_SESSION
end

#resetObject



117
118
119
120
121
# File 'lib/terminalwire/v2/rails.rb', line 117

def reset
  @context.file.delete(@config_file_path)
rescue StandardError
  nil
end

#write(config) ⇒ Object



133
134
135
136
# File 'lib/terminalwire/v2/rails.rb', line 133

def write(config)
  token = encryptor.encrypt_and_sign(config, expires_in: @expires_in)
  @context.file.write(@config_file_path, token)
end