dotenvcrypt 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1ead91de7add08540af64e47ff6f32f8c0a3e0f552fdd6b5a04a0485dc618909
4
- data.tar.gz: fef0232606440a15f36b5f9e3ab39588df74780cd110697e53c4cfc1a5ca1f48
3
+ metadata.gz: 111cda13c2044b722ef62e9fb14425ad5c02476934d81a65708e1c72e031a11c
4
+ data.tar.gz: '0084c2763785031c2bf4806d03dcad28c31a9480953c66438dfcf1e18729f5db'
5
5
  SHA512:
6
- metadata.gz: 64aa077f96823950f7c354f9a08f61c671e042d2fefd17df84c5776f53cbc21c590b6716c22d3d696baa2cf67804142b6a44cef5fc7acde87c5fc182f4ffbbea
7
- data.tar.gz: 2600fa1f56ae93551ad6649f7dca32cebb2dbfd3bec63c5501a5c836162110d4aa2eff7efaccde382a0044c312ac0dce44cb890dc54c9e0b1bde9eea9b02249d
6
+ metadata.gz: 04c1dbfae62eebd97256f895692ba7cac8f55214cdebea571a2157bf9108848c49c5359c1e46ce73fcc950840f6bb30e3306c9f00e4b187b3c6338d0c7f81a5b
7
+ data.tar.gz: 43a7f64f3487e7787cfab7bb13937937fb8fee32552c4b1898233c3d5b6f4f21e9c7ba254f8742b8420e05097eac591d87ce2039ea572a285f0c412f8ed817ae
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # dotenvcrypt 🛡️🔐
1
+ # dotenvcrypt
2
2
 
3
3
  **Securely encrypt, manage, and load your `.env` files in public repositories.**
4
4
 
@@ -6,15 +6,16 @@
6
6
 
7
7
  ---
8
8
 
9
- ## 🚀 Features
9
+ ## Features
10
10
 
11
- - Encrypt `.env` files into `.env.enc` for safe storage in Git.
12
- - Decrypt and load environment variables securely into the shell.
13
- - Edit encrypted `.env.enc`, then re-encrypt after saving.
11
+ - Encrypt `.env` files into `.env.enc` for safe storage in Git
12
+ - Decrypt and load environment variables securely into the shell
13
+ - Edit encrypted `.env.enc`, then re-encrypt after saving
14
+ - Use as a CLI tool or as a Ruby library
14
15
 
15
16
  ---
16
17
 
17
- ## 📦 Installation
18
+ ## Installation
18
19
 
19
20
  ### Using Homebrew (recommended)
20
21
 
@@ -22,7 +23,15 @@
22
23
  brew install namolnad/formulae/dotenvcrypt
23
24
  ```
24
25
 
25
- ## 🔧 Usage
26
+ ### Using RubyGems
27
+
28
+ ```bash
29
+ gem install dotenvcrypt
30
+ ```
31
+
32
+ ---
33
+
34
+ ## CLI Usage
26
35
 
27
36
  ### Basic Commands
28
37
 
@@ -45,13 +54,13 @@ eval "$(dotenvcrypt decrypt .env.enc)"
45
54
  dotenvcrypt looks for your encryption key in these locations (in order):
46
55
 
47
56
  1. Command line argument: `--key YOUR_SECRET_KEY`
48
- 1. Environment variable: `DOTENVCRYPT_KEY`
49
- 1. File: `./.dotenvcrypt.key` (in the current directory)
50
- 1. File: `$XDG_CONFIG_HOME/dotenvcrypt/secret.key` (or `$HOME/.config/dotenvcrypt/secret.key`)
51
- 1. File: `$HOME/.dotenvcrypt.key`
52
- 1. Interactive prompt (if no key is found)
57
+ 2. Environment variable: `DOTENVCRYPT_KEY`
58
+ 3. File: `./.dotenvcrypt.key` (in the current directory)
59
+ 4. File: `$XDG_CONFIG_HOME/dotenvcrypt/secret.key` (or `$HOME/.config/dotenvcrypt/secret.key`)
60
+ 5. File: `$HOME/.dotenvcrypt.key`
61
+ 6. Interactive prompt (if no key is found)
53
62
 
54
- ### Real-World Example
63
+ ### Shell Integration Example
55
64
 
56
65
  Add this to your shell profile (`.zshrc`, `.bashrc`, etc.) to automatically load variables:
57
66
 
@@ -65,10 +74,43 @@ if [[ ! -f $dotenvcrypt_key_path || ! -s $dotenvcrypt_key_path ]]; then
65
74
  chmod 600 $dotenvcrypt_key_path
66
75
  fi
67
76
 
68
- # Load encrypted environment variables if envcrypt is installed
77
+ # Load encrypted environment variables if dotenvcrypt is installed
69
78
  if command -v dotenvcrypt &> /dev/null; then
70
79
  set -a # automatically export all variables
71
80
  eval "$(dotenvcrypt decrypt $HOME/.env.enc)"
72
81
  set +a # stop automatically exporting
73
82
  fi
74
83
  ```
84
+
85
+ ---
86
+
87
+ ## Library Usage
88
+
89
+ You can also use dotenvcrypt as a Ruby library in your applications:
90
+
91
+ ```ruby
92
+ require 'dotenvcrypt'
93
+
94
+ # Load encrypted .env file directly into ENV
95
+ Dotenvcrypt.load_env('.env.enc', key: 'your-secret-key')
96
+
97
+ # Now your environment variables are available
98
+ puts ENV['DATABASE_URL']
99
+ ```
100
+
101
+ For more advanced library usage, see the [dotenvcrypt-core](https://github.com/namolnad/dotenvcrypt/tree/main/core) documentation.
102
+
103
+ ---
104
+
105
+ ## Security
106
+
107
+ - Uses AES-256 in GCM (Galois/Counter Mode) for authenticated encryption
108
+ - Generates secure random initialization vectors (IV) for each encryption
109
+ - Provides tamper detection via authentication tags
110
+ - Keys are hashed with SHA256 before use
111
+
112
+ ---
113
+
114
+ ## License
115
+
116
+ MIT
data/bin/dotenvcrypt CHANGED
@@ -49,17 +49,19 @@ if command.nil?
49
49
  exit 1
50
50
  end
51
51
 
52
- dotenvcrypt = Dotenvcrypt::Manager.new(options[:key])
52
+ # Fetch encryption key
53
+ key = Dotenvcrypt::CLI::KeyManager.fetch_key(options[:key])
54
+ commands = Dotenvcrypt::CLI::Commands.new(key: key)
53
55
 
54
56
  case command
55
57
  when 'encrypt'
56
58
  input_file = ARGV[0] || UNENCRYPTED_FILE
57
59
  output_file = ARGV[1] || ENCRYPTED_FILE
58
- dotenvcrypt.encrypt_file(input_file, output_file)
60
+ commands.encrypt_file(input_file, output_file)
59
61
  when 'decrypt'
60
- dotenvcrypt.decrypt_file(ARGV[0] || ENCRYPTED_FILE)
62
+ commands.decrypt_file(ARGV[0] || ENCRYPTED_FILE)
61
63
  when 'edit'
62
- dotenvcrypt.edit_file(ARGV[0] || ENCRYPTED_FILE)
64
+ commands.edit_file(ARGV[0] || ENCRYPTED_FILE)
63
65
  else
64
66
  puts "Unknown command: #{command}"
65
67
  puts opt_parser
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tempfile'
4
+
5
+ module Dotenvcrypt
6
+ module CLI
7
+ class Commands
8
+ def initialize(key:)
9
+ @key = key
10
+ @encryptor = Dotenvcrypt::Core::Encryptor.new(key)
11
+ end
12
+
13
+ # Encrypt an existing `.env` file
14
+ def encrypt_file(unencrypted_file, encrypted_file)
15
+ unless File.exist?(unencrypted_file)
16
+ puts "❌ File not found: #{unencrypted_file}"
17
+ exit(1)
18
+ end
19
+
20
+ encrypted_data = @encryptor.encrypt(File.read(unencrypted_file))
21
+ File.write(encrypted_file, encrypted_data)
22
+
23
+ puts "🔒 Encrypted #{unencrypted_file} → #{encrypted_file}"
24
+ rescue Dotenvcrypt::Core::EncryptionError => e
25
+ puts "❌ Encryption failed: #{e.message}"
26
+ exit(1)
27
+ end
28
+
29
+ # Decrypt an encrypted `.env` file and print to stdout
30
+ def decrypt_file(encrypted_file)
31
+ unless File.exist?(encrypted_file)
32
+ puts "❌ File not found: #{encrypted_file}"
33
+ exit(1)
34
+ end
35
+
36
+ decrypted_content = @encryptor.decrypt(File.read(encrypted_file))
37
+ puts decrypted_content
38
+ rescue Dotenvcrypt::Core::DecryptionError => e
39
+ puts "❌ Decryption failed: #{e.message}"
40
+ exit(1)
41
+ rescue Dotenvcrypt::Core::InvalidFormatError => e
42
+ puts "❌ #{e.message}"
43
+ exit(1)
44
+ end
45
+
46
+ # Edit decrypted env, then re-encrypt
47
+ def edit_file(encrypted_file)
48
+ unless File.exist?(encrypted_file)
49
+ puts "❌ File not found: #{encrypted_file}"
50
+ exit(1)
51
+ end
52
+
53
+ temp_file = Tempfile.new('dotenvcrypt')
54
+
55
+ begin
56
+ original_content = @encryptor.decrypt(File.read(encrypted_file))
57
+
58
+ File.write(temp_file.path, original_content)
59
+
60
+ puts "Waiting for file to be saved. Abort with Ctrl-C."
61
+
62
+ system("#{ENV['EDITOR'] || 'vim'} #{temp_file.path}")
63
+
64
+ updated_content = File.read(temp_file.path)
65
+
66
+ if updated_content != original_content
67
+ encrypted_data = @encryptor.encrypt(updated_content)
68
+ File.write(encrypted_file, encrypted_data)
69
+ end
70
+
71
+ puts "🔒 Encrypted and saved #{encrypted_file}"
72
+ rescue Dotenvcrypt::Core::DecryptionError => e
73
+ puts "❌ Decryption failed: #{e.message}"
74
+ exit(1)
75
+ rescue Dotenvcrypt::Core::EncryptionError => e
76
+ puts "❌ Encryption failed: #{e.message}"
77
+ exit(1)
78
+ rescue Dotenvcrypt::Core::InvalidFormatError => e
79
+ puts "❌ #{e.message}"
80
+ exit(1)
81
+ ensure
82
+ temp_file.unlink if temp_file
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tty-prompt'
4
+
5
+ module Dotenvcrypt
6
+ module CLI
7
+ class KeyManager
8
+ # Determine config directory following XDG Base Directory Specification
9
+ CONFIG_HOME = ENV['XDG_CONFIG_HOME'] || "#{ENV['HOME']}/.config"
10
+ CONFIG_DIR = "#{CONFIG_HOME}/dotenvcrypt".freeze
11
+
12
+ # Check multiple locations in order of preference
13
+ ENCRYPTION_KEY_LOCATIONS = [
14
+ "#{Dir.pwd}/.dotenvcrypt.key", # Project-specific key (current directory)
15
+ "#{CONFIG_DIR}/secret.key", # XDG standard location
16
+ "#{ENV['HOME']}/.dotenvcrypt.key" # Legacy location (for backward compatibility)
17
+ ].freeze
18
+
19
+ def self.fetch_key(cli_key = nil)
20
+ return cli_key if cli_key && !cli_key.empty?
21
+ return ENV['DOTENVCRYPT_KEY'] if ENV['DOTENVCRYPT_KEY'] && !ENV['DOTENVCRYPT_KEY'].empty?
22
+
23
+ ENCRYPTION_KEY_LOCATIONS.each do |key_file|
24
+ return File.read(key_file).strip if File.exist?(key_file)
25
+ end
26
+
27
+ TTY::Prompt.new.ask("Enter encryption key:", echo: false)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -1,3 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ # During gem build or in monorepo: read version from core directly
4
+ core_version_file = File.expand_path('../../core/lib/dotenvcrypt/core/version.rb', __dir__)
5
+ if File.exist?(core_version_file)
6
+ # Load from monorepo structure
7
+ require_relative '../../core/lib/dotenvcrypt/core/version'
8
+ else
9
+ # For gem installation: load from installed dotenvcrypt-core gem
10
+ require 'dotenvcrypt/core/version'
11
+ end
12
+
1
13
  module Dotenvcrypt
2
- VERSION = "0.6.0"
14
+ VERSION = Core::VERSION
3
15
  end
data/lib/dotenvcrypt.rb CHANGED
@@ -1,3 +1,25 @@
1
- require_relative "dotenvcrypt/encryptor"
2
- require_relative "dotenvcrypt/manager"
3
- require_relative "dotenvcrypt/version"
1
+ # frozen_string_literal: true
2
+
3
+ # For development/monorepo: try to load from relative path first
4
+ begin
5
+ require_relative '../../core/lib/dotenvcrypt-core'
6
+ rescue LoadError
7
+ # For gem installation: load from installed dotenvcrypt-core gem
8
+ require 'dotenvcrypt-core'
9
+ end
10
+
11
+ require 'tty-prompt'
12
+ require 'tty-command'
13
+ require_relative 'dotenvcrypt/version'
14
+ require_relative 'dotenvcrypt/cli/key_manager'
15
+ require_relative 'dotenvcrypt/cli/commands'
16
+
17
+ module Dotenvcrypt
18
+ # Re-export core functionality (if not already defined)
19
+ Core = Dotenvcrypt::Core unless defined?(Core)
20
+
21
+ # Convenience method for library usage
22
+ def self.load_env(encrypted_file_path, key:)
23
+ Core.load_env(encrypted_file_path, key: key)
24
+ end
25
+ end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dotenvcrypt
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dan Loman
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-03-15 00:00:00.000000000 Z
11
+ date: 2025-12-30 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: dotenvcrypt-core
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '='
18
+ - !ruby/object:Gem::Version
19
+ version: 0.7.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '='
25
+ - !ruby/object:Gem::Version
26
+ version: 0.7.0
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: tty-prompt
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -51,8 +65,8 @@ files:
51
65
  - README.md
52
66
  - bin/dotenvcrypt
53
67
  - lib/dotenvcrypt.rb
54
- - lib/dotenvcrypt/encryptor.rb
55
- - lib/dotenvcrypt/manager.rb
68
+ - lib/dotenvcrypt/cli/commands.rb
69
+ - lib/dotenvcrypt/cli/key_manager.rb
56
70
  - lib/dotenvcrypt/version.rb
57
71
  homepage: https://github.com/namolnad/dotenvcrypt
58
72
  licenses:
@@ -1,65 +0,0 @@
1
- module Dotenvcrypt
2
- class Encryptor
3
- def initialize(encryption_key)
4
- @encryption_key = encryption_key
5
- end
6
-
7
- # Encrypt file
8
- def encrypt(data)
9
- cipher = OpenSSL::Cipher::AES256.new(:GCM)
10
- cipher.encrypt
11
- key = OpenSSL::Digest::SHA256.digest(encryption_key)
12
- cipher.key = key
13
-
14
- # Generate a secure random IV (12 bytes is recommended for GCM)
15
- iv = cipher.random_iv
16
-
17
- encrypted = cipher.update(data) + cipher.final
18
-
19
- # Get the authentication tag (16 bytes)
20
- auth_tag = cipher.auth_tag
21
-
22
- # Combine IV, encrypted data, and auth tag
23
- combined = iv + auth_tag + encrypted
24
-
25
- # Convert to base64 for readability
26
- Base64.strict_encode64(combined)
27
- end
28
-
29
- # Decrypt file
30
- def decrypt(encoded_data)
31
- begin
32
- data = Base64.strict_decode64(encoded_data)
33
- rescue ArgumentError
34
- puts "❌ Decryption failed. File is not in valid base64 format."
35
- exit(1)
36
- end
37
-
38
- # For GCM mode:
39
- # - IV is 12 bytes
40
- # - Auth tag is 16 bytes
41
- iv_size = 12
42
- auth_tag_size = 16
43
-
44
- # Extract the components
45
- iv = data[0...iv_size]
46
- auth_tag = data[iv_size...(iv_size + auth_tag_size)]
47
- encrypted_data = data[(iv_size + auth_tag_size)..-1]
48
-
49
- cipher = OpenSSL::Cipher::AES256.new(:GCM)
50
- cipher.decrypt
51
- cipher.key = OpenSSL::Digest::SHA256.digest(encryption_key)
52
- cipher.iv = iv
53
- cipher.auth_tag = auth_tag
54
-
55
- cipher.update(encrypted_data) + cipher.final
56
- rescue OpenSSL::Cipher::CipherError
57
- puts "❌ Decryption failed. Invalid key, corrupted file, or tampering detected."
58
- exit(1)
59
- end
60
-
61
- private
62
-
63
- attr_reader :encryption_key
64
- end
65
- end
@@ -1,90 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'base64'
4
- require 'openssl'
5
- require 'tempfile'
6
- require 'tty-prompt'
7
- require 'tty-command'
8
- require_relative 'encryptor'
9
-
10
- module Dotenvcrypt
11
- class Manager
12
- # Determine config directory following XDG Base Directory Specification
13
- CONFIG_HOME = ENV['XDG_CONFIG_HOME'] || "#{ENV['HOME']}/.config"
14
- CONFIG_DIR = "#{CONFIG_HOME}/dotenvcrypt".freeze
15
-
16
- # Check multiple locations in order of preference
17
- ENCRYPTION_KEY_LOCATIONS = [
18
- "#{Dir.pwd}/.dotenvcrypt.key", # Project-specific key (current directory)
19
- "#{CONFIG_DIR}/secret.key", # XDG standard location
20
- "#{ENV['HOME']}/.dotenvcrypt.key" # Legacy location (for backward compatibility)
21
- ].freeze
22
-
23
- def initialize(encryption_key = nil)
24
- @encryption_key = encryption_key
25
- @encryptor = Encryptor.new(fetch_key)
26
- end
27
-
28
- # Encrypt an existing `.env` file
29
- def encrypt_file(unencrypted_file, encrypted_file)
30
- unless File.exist?(unencrypted_file)
31
- puts "❌ File not found: #{unencrypted_file}"
32
- exit(1)
33
- end
34
-
35
- File.open(encrypted_file, 'w') do |f|
36
- f.write encryptor.encrypt(File.read(unencrypted_file))
37
- end
38
-
39
- puts "🔒 Encrypted #{unencrypted_file} → #{encrypted_file}"
40
- end
41
-
42
- # Decrypt an encrypted `.env` file and print to stdout
43
- def decrypt_file(encrypted_file)
44
- puts encryptor.decrypt(File.read(encrypted_file))
45
- end
46
-
47
- # Edit decrypted env, then re-encrypt
48
- def edit_file(encrypted_file)
49
- temp_file = Tempfile.new('dotenvcrypt')
50
-
51
- original_content = encryptor.decrypt(File.read(encrypted_file))
52
-
53
- File.open(temp_file.path, 'w') do |f|
54
- f.write original_content
55
- end
56
-
57
- puts "Waiting for file to be saved. Abort with Ctrl-C."
58
-
59
- system("#{ENV['EDITOR'] || 'vim'} #{temp_file.path}")
60
-
61
- updated_content = File.read(temp_file.path)
62
-
63
- if updated_content != original_content
64
- File.open(encrypted_file, 'w') do |f|
65
- f.write encryptor.encrypt(updated_content)
66
- end
67
- end
68
-
69
- puts "🔒 Encrypted and saved #{encrypted_file}"
70
- ensure
71
- temp_file.unlink if temp_file
72
- end
73
-
74
- private
75
-
76
- attr_reader :encryption_key, :encryptor
77
-
78
- # Get encryption key: From CLI arg OR encryption-key file OR securely prompt from stdin
79
- def fetch_key
80
- return encryption_key if encryption_key && !encryption_key.empty?
81
- return ENV['DOTENVCRYPT_KEY'] if ENV['DOTENVCRYPT_KEY'] && !ENV['DOTENVCRYPT_KEY'].empty?
82
- ENCRYPTION_KEY_LOCATIONS.each do |key_file|
83
- return File.read(key_file).strip if File.exist?(key_file)
84
- end
85
-
86
- TTY::Prompt.new.ask("Enter encryption key:", echo: false)
87
- end
88
- end
89
- end
90
-