Module: CookieCache

Defined in:
lib/CookieCache.rb

Overview

On-disk cache for Medium / Cloudflare cookies captured by ChromeAuth. Stored at ~/.zmediumtomarkdown so subsequent runs can reuse sid/uid (long-lived) and ride out a still-valid cf_clearance/_cfuvid without prompting again.

Encrypted at rest with AES-256-GCM using a fixed key shipped with the gem. The key is constant on purpose — this is *obfuscation against casual file-system snoops*, not protection from an attacker who has the gem source. The file is also written 0600.

On-disk layout (binary):

bytes 0..11   : 12-byte IV     (random per write)
bytes 12..27  : 16-byte tag    (GCM auth tag)
bytes 28..    : ciphertext

The path can be overridden with ZMEDIUM_COOKIE_CACHE_PATH (used by tests and power users who want the cache in a different location).

Constant Summary collapse

PATH_ENV =
'ZMEDIUM_COOKIE_CACHE_PATH'.freeze
DEFAULT_BASENAME =
'.zmediumtomarkdown'.freeze
CIPHER =
'aes-256-gcm'.freeze
SECRET =

32 bytes → AES-256

'r3n2wJAX8o944MqFVZPwirjUGZ9A7mII'.freeze
IV_LEN =
12
TAG_LEN =
16

Class Method Summary collapse

Class Method Details

.clearObject



66
67
68
69
70
# File 'lib/CookieCache.rb', line 66

def clear
    File.unlink(path) if File.exist?(path)
rescue Errno::ENOENT
    # already gone
end

.decrypt(blob) ⇒ Object



81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/CookieCache.rb', line 81

def decrypt(blob)
    raise 'cache blob too short' if blob.nil? || blob.bytesize < IV_LEN + TAG_LEN
    iv  = blob.byteslice(0, IV_LEN)
    tag = blob.byteslice(IV_LEN, TAG_LEN)
    ct  = blob.byteslice(IV_LEN + TAG_LEN, blob.bytesize - IV_LEN - TAG_LEN)
    cipher = OpenSSL::Cipher.new(CIPHER).decrypt
    cipher.key = SECRET
    cipher.iv  = iv
    cipher.auth_tag  = tag
    cipher.auth_data = ''
    cipher.update(ct) + cipher.final
end

.encrypt(plaintext) ⇒ Object



72
73
74
75
76
77
78
79
# File 'lib/CookieCache.rb', line 72

def encrypt(plaintext)
    cipher = OpenSSL::Cipher.new(CIPHER).encrypt
    cipher.key = SECRET
    iv = cipher.random_iv  # 12 bytes
    cipher.auth_data = ''
    ct = cipher.update(plaintext) + cipher.final
    iv + cipher.auth_tag + ct
end

.loadObject

Returns hash of cached cookies. Missing file or unreadable / corrupt blob (wrong key, truncated, tampered) returns {} — never raises, so the caller can treat the cache as best-effort.



41
42
43
44
45
46
47
48
# File 'lib/CookieCache.rb', line 41

def load
    return {} unless File.exist?(path)
    plaintext = decrypt(File.binread(path))
    parsed = JSON.parse(plaintext)
    parsed.is_a?(Hash) ? parsed : {}
rescue StandardError
    {}
end

.pathObject



32
33
34
35
36
# File 'lib/CookieCache.rb', line 32

def path
    override = ENV[PATH_ENV].to_s
    return override unless override.empty?
    File.join(Dir.home, DEFAULT_BASENAME)
end

.save(hash) ⇒ Object

Atomic write: encrypt the JSON blob, write to a sibling tmp file at 0600, rename. Best-effort: any IO/encryption error is swallowed (cache is convenience, not source of truth — losing a write should not abort the run).



54
55
56
57
58
59
60
61
62
63
64
# File 'lib/CookieCache.rb', line 54

def save(hash)
    return unless hash.is_a?(Hash) && !hash.empty?
    FileUtils.mkdir_p(File.dirname(path))
    tmp = "#{path}.tmp.#{Process.pid}"
    File.open(tmp, File::WRONLY | File::CREAT | File::TRUNC | File::BINARY, 0o600) do |f|
        f.write(encrypt(JSON.generate(hash)))
    end
    File.rename(tmp, path)
rescue StandardError
    File.unlink(tmp) if defined?(tmp) && tmp && File.exist?(tmp)
end