Skip to content

Commit 8392acd

Browse files
committed
Encrypted source URLs suport
1 parent 9c4f23f commit 8392acd

8 files changed

Lines changed: 178 additions & 4 deletions

File tree

.rubocop.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ Style/DoubleNegation:
2929
Layout/LineLength:
3030
Max: 100
3131

32+
Metrics/ClassLength:
33+
Max: 120
34+
3235
Metrics/BlockLength:
3336
Exclude:
3437
- spec/**/*

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,9 @@ end
9898
- **use_short_options** (`IMGPROXY_USE_SHORT_OPTIONS`) - Use short processing options names (`rs` for `resize`, `g` for `gravity`, etc). Default: true.
9999
- **base64_encode_urls** (`IMGPROXY_BASE64_ENCODE_URLS`) - Encode source URLs to base64. Default: false.
100100
- **always_escape_plain_urls** (`IMGPROXY_ALWAYS_ESCAPE_PLAIN_URLS`) - Always escape plain source URLs even when ones don't need to be escaped. Default: false.
101+
- **source_url_encryption_key** (`IMGPROXY_SOURCE_URL_ENCRYPTION_KEY`) - Hex-encoded source URL encryption key. Default: `nil`.
102+
- **raw_source_url_encryption_key** (`IMGPROXY_RAW_SOURCE_URL_ENCRYPTION_KEY`) - Raw (not hex-encoded) source URL encryption key. Default: `nil`.
103+
- **always_encrypt_source_urls** (`IMGPROXY_ALWAYS_ENCRYPT_SOURCE_URLS`) - Always encrypt source URLs. Default: false.
101104
- **use_s3_urls** (`IMGPROXY_USE_S3_URLS`) - Use `s3://...` source URLs for Active Storage and Shrine attachments stored in Amazon S3. Default: false.
102105
- **use_gcs_urls** (`IMGPROXY_USE_GCS_URLS`) - Use `gs://...` source URLs for Active Storage and Shrine attachments stored in Google Cloud Storage. Default: false.
103106
- **gcs_bucket** (`IMGPROXY_GCS_BUCKET`) - Google Cloud Storage bucket name. Default: `nil`.
@@ -328,6 +331,8 @@ Imgproxy.url_for(
328331
- `base64_encode_url` — per-call redefinition of `base64_encode_urls` config.
329332
- `escape_plain_url` — per-call redefinition of `always_escape_plain_urls` config.
330333
- `use_short_options` — per-call redefinition of `use_short_options` config.
334+
- `encrypt_source_url` - per-call redefinition of `always_encrypt_source_urls` config.
335+
- `source_url_encryption_iv` - an initialization vector (IV) to be used for the source URL encryption if encryption is needed. If not specified, a random IV is used.
331336

332337
## Getting the image info
333338

@@ -392,6 +397,8 @@ Imgproxy.configure do |config|
392397
pro.endpoint = "https://pro.imgproxy.com/"
393398
pro.key = ENV["IMGPROXY_PRO_KEY"]
394399
pro.salt = ENV["IMGPROXY_PRO_SALT"]
400+
pro.source_url_encryption_key = ENV["IMGPROXY_PRO_ENCRYPTION_KEY"]
401+
pro.always_encrypt_source_urls = true
395402
end
396403
end
397404
```
@@ -405,9 +412,11 @@ services:
405412
endpoint: "https://pro.imgproxy.com/"
406413
key: <%= ENV["IMGPROXY_PRO_KEY"] %>
407414
salt: <%= ENV["IMGPROXY_PRO_SALT"] %>
415+
source_url_encryption_key: ENV["IMGPROXY_PRO_ENCRYPTION_KEY"]
416+
always_encrypt_source_urls: true
408417
```
409418
410-
If you don't specify `key`, `salt`, `endpoint`, or `signature_size`, they are inherited from the global configuration.
419+
If you don't specify `key`, `salt`, `endpoint`, `signature_size`, `source_url_encryption_key`, or `always_encrypt_source_urls`, they are inherited from the global configuration.
411420

412421
Pass the `service` option to `url_for` and `info_url_for`:
413422

lib/imgproxy.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ def configure
105105
# @option options [Boolean] :use_short_options
106106
# @option options [Boolean] :base64_encode_urls
107107
# @option options [Boolean] :escape_plain_url
108+
# @option options [Boolean] :encrypt_source_url
109+
# @option options [Boolean] :source_url_encryption_iv
108110
# @see https://docs.imgproxy.net/generating_the_url_advanced?id=processing-options
109111
# Available imgproxy URL processing options and their arguments
110112
def url_for(image, options = {})

lib/imgproxy/builder.rb

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ module Imgproxy
1919
# builder.url_for("http://images.example.com/images/image2.jpg")
2020
class Builder
2121
class UnknownServiceError < StandardError; end
22+
class InvalidEncryptionKeyError < StandardError; end
2223

2324
# @param [Hash] options Processing options
2425
# @see Imgproxy.url_for
@@ -62,15 +63,21 @@ def info_url_for(image)
6263
attr_reader :service
6364

6465
NEED_ESCAPE_RE = /[@?% ]|[^\p{Ascii}]/.freeze
66+
AES_SIZES = { 32 => 256, 24 => 196, 16 => 128 }.freeze
6567

68+
# rubocop: disable Metrics/AbcSize
6669
def extract_builder_options(options)
6770
@service = options.delete(:service)&.to_sym || :default
6871

6972
@use_short_options = not_nil_or(options.delete(:use_short_options), config.use_short_options)
7073
@base64_encode_url = not_nil_or(options.delete(:base64_encode_url), config.base64_encode_urls)
7174
@escape_plain_url =
7275
not_nil_or(options.delete(:escape_plain_url), config.always_escape_plain_urls)
76+
@encrypt_source_url =
77+
not_nil_or(options.delete(:encrypt_source_url), service_config.always_encrypt_source_urls)
78+
@source_url_encryption_iv = options.delete(:source_url_encryption_iv)
7379
end
80+
# rubocop: enable Metrics/AbcSize
7481

7582
def processing_options
7683
@processing_options ||= @options.map do |key, value|
@@ -81,7 +88,9 @@ def processing_options
8188
def url(image, ext: nil)
8289
url = config.url_adapters.url_of(image)
8390

84-
@base64_encode_url ? base64_url_for(url, ext: ext) : plain_url_for(url, ext: ext)
91+
return encrypted_url_for(url, ext: ext) if @encrypt_source_url
92+
return base64_url_for(url, ext: ext) if @base64_encode_url
93+
plain_url_for(url, ext: ext)
8594
end
8695

8796
def plain_url_for(url, ext: nil)
@@ -96,10 +105,33 @@ def base64_url_for(url, ext: nil)
96105
ext ? "#{encoded_url}.#{ext}" : encoded_url
97106
end
98107

108+
def encrypted_url_for(url, ext: nil)
109+
cipher = build_cipher
110+
111+
iv = @source_url_encryption_iv || cipher.random_iv
112+
cipher.iv = iv
113+
114+
"enc/#{base64_url_for(iv + cipher.update(url) + cipher.final, ext: ext)}"
115+
end
116+
99117
def need_escape_url?(url)
100118
@escape_plain_url || url.match?(NEED_ESCAPE_RE)
101119
end
102120

121+
def build_cipher
122+
key = encryption_key.to_s
123+
124+
aes_size = AES_SIZES.fetch(key.length) do
125+
raise InvalidEncryptionKeyError,
126+
"Encryption key should be 16/24/32 bytes long, now - #{key.length}"
127+
end
128+
129+
OpenSSL::Cipher::AES.new(aes_size, :CBC).tap do |cipher|
130+
cipher.encrypt
131+
cipher.key = key
132+
end
133+
end
134+
103135
def option_alias(name)
104136
return name unless @use_short_options
105137

@@ -135,6 +167,10 @@ def signature_size
135167
service_config.signature_size
136168
end
137169

170+
def encryption_key
171+
service_config.raw_source_url_encryption_key
172+
end
173+
138174
def not_nil_or(value, fallback)
139175
value.nil? ? fallback : value
140176
end

lib/imgproxy/config.rb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,30 @@ def signature_size=(value)
9999
service(:default).signature_size = value
100100
end
101101

102+
def source_url_encryption_key
103+
service(:default).source_url_encryption_key
104+
end
105+
106+
def source_url_encryption_key=(value)
107+
service(:default).source_url_encryption_key = value
108+
end
109+
110+
def raw_source_url_encryption_key
111+
service(:default).raw_source_url_encryption_key
112+
end
113+
114+
def raw_source_url_encryption_key=(value)
115+
service(:default).raw_source_url_encryption_key = value
116+
end
117+
118+
def always_encrypt_source_urls
119+
service(:default).always_encrypt_source_urls
120+
end
121+
122+
def always_encrypt_source_urls=(value)
123+
service(:default).always_encrypt_source_urls = value
124+
end
125+
102126
def service(name)
103127
services[name.to_sym] ||= services[:default].dup
104128

lib/imgproxy/service_config.rb

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@ module Imgproxy
2121
# @!attribute signature_size
2222
# imgproxy signature size. Defaults to 32
2323
# @return [String]
24+
# @!attribute source_url_encryption_key
25+
# imgproxy hex-encoded source URL encryption key
26+
# @return [String]
27+
# @!attribute raw_source_url_encryption_key
28+
# Decoded source URL encryption key
29+
# @return [String]
30+
# @!attribute always_encrypt_source_urls
31+
# Always encrypt source URLs. Defaults to false
32+
# @return [String]
2433
#
2534
# @see Imgproxy::Config
2635
class ServiceConfig < Anyway::Config
@@ -33,21 +42,31 @@ class ServiceConfig < Anyway::Config
3342
:salt,
3443
:raw_key,
3544
:raw_salt,
45+
:source_url_encryption_key,
46+
:raw_source_url_encryption_key,
3647
signature_size: 32,
48+
always_encrypt_source_urls: false,
3749
)
3850

3951
coerce_types endpoint: :string,
4052
key: :string,
4153
salt: :string,
4254
raw_key: :string,
4355
raw_salt: :string,
44-
signature_size: :integer
56+
signature_size: :integer,
57+
source_url_encryption_key: :string,
58+
raw_source_url_encryption_key: :string,
59+
always_encrypt_source_urls: :boolean
4560

4661
alias_method :set_key, :key=
4762
alias_method :set_raw_key, :raw_key=
4863
alias_method :set_salt, :salt=
4964
alias_method :set_raw_salt, :raw_salt=
50-
private :set_key, :set_raw_key, :set_salt, :set_raw_salt
65+
alias_method :set_source_url_encryption_key, :source_url_encryption_key=
66+
alias_method :set_raw_source_url_encryption_key, :raw_source_url_encryption_key=
67+
68+
private :set_key, :set_raw_key, :set_salt, :set_raw_salt,
69+
:set_source_url_encryption_key, :set_raw_source_url_encryption_key
5170

5271
def key=(value)
5372
value = value&.to_s
@@ -72,5 +91,17 @@ def raw_salt=(value)
7291
super(value)
7392
set_salt(value&.unpack("H*")&.first)
7493
end
94+
95+
def source_url_encryption_key=(value)
96+
value = value&.to_s
97+
super(value)
98+
set_raw_source_url_encryption_key(value && [value].pack("H*"))
99+
end
100+
101+
def raw_source_url_encryption_key=(value)
102+
value = value&.to_s
103+
super(value)
104+
set_source_url_encryption_key(value&.unpack("H*")&.first)
105+
end
75106
end
76107
end

spec/imgproxy_spec.rb

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@
122122
format: :webp,
123123
return_attachment: true,
124124
expires: Time.at(4810374983),
125+
source_url_encryption_iv: "1234567890123456",
125126
}
126127
end
127128

@@ -595,6 +596,72 @@
595596
end
596597
end
597598
end
599+
600+
context "when always_encrypt_source_urls config is true" do
601+
before do
602+
described_class.config.source_url_encryption_key =
603+
"1eb5b0e971ad7f45324c1bb15c947cb207c43152fa5c6c7f35c4f36e0c18e0f1"
604+
described_class.config.always_encrypt_source_urls = true
605+
end
606+
607+
it "encrypts source URL" do
608+
expect(url).to end_with(
609+
"/enc/MTIzNDU2Nzg5MDEy/MzQ1Np6L0YlR92XD/i3aaVA5KINDMHKXf/LUaQ1N0ae5N7JjBZ.webp",
610+
)
611+
end
612+
613+
context "without encryption key specified" do
614+
before { described_class.config.source_url_encryption_key = nil }
615+
616+
it "rises an error" do
617+
expect { url }.to raise_error(Imgproxy::Builder::InvalidEncryptionKeyError)
618+
end
619+
end
620+
621+
context "with encryption key of invalid length" do
622+
before do
623+
described_class.config.source_url_encryption_key =
624+
"1eb5b0e971ad7f45324c1bb15c947cb207c43152fa5c6c7f35c4f36e0c18"
625+
end
626+
627+
it "rises an error" do
628+
expect { url }.to raise_error(Imgproxy::Builder::InvalidEncryptionKeyError)
629+
end
630+
end
631+
end
632+
633+
context "when encrypt_source_url option is true" do
634+
before do
635+
described_class.config.source_url_encryption_key =
636+
"1eb5b0e971ad7f45324c1bb15c947cb207c43152fa5c6c7f35c4f36e0c18e0f1"
637+
options[:encrypt_source_url] = true
638+
end
639+
640+
it "encrypts source URL" do
641+
expect(url).to end_with(
642+
"/enc/MTIzNDU2Nzg5MDEy/MzQ1Np6L0YlR92XD/i3aaVA5KINDMHKXf/LUaQ1N0ae5N7JjBZ.webp",
643+
)
644+
end
645+
646+
context "without encryption key specified" do
647+
before { described_class.config.source_url_encryption_key = nil }
648+
649+
it "rises an error" do
650+
expect { url }.to raise_error(Imgproxy::Builder::InvalidEncryptionKeyError)
651+
end
652+
end
653+
654+
context "with encryption key of invalid length" do
655+
before do
656+
described_class.config.source_url_encryption_key =
657+
"1eb5b0e971ad7f45324c1bb15c947cb207c43152fa5c6c7f35c4f36e0c18"
658+
end
659+
660+
it "rises an error" do
661+
expect { url }.to raise_error(Imgproxy::Builder::InvalidEncryptionKeyError)
662+
end
663+
end
664+
end
598665
end
599666

600667
describe ".info_url_for" do

spec/spec_helper.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@
4141
c.use_gcs_urls = false
4242
c.gcs_bucket = nil
4343
c.shrine_host = nil
44+
c.source_url_encryption_key = nil
45+
c.always_encrypt_source_urls = false
4446

4547
c.url_adapters.clear!
4648
end

0 commit comments

Comments
 (0)