active_storage_encryption 0.1.0 → 0.2.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.
Files changed (142) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +22 -3
  3. data/Rakefile +0 -2
  4. data/config/routes.rb +1 -1
  5. data/gemfiles/rails_7.gemfile.lock +9 -1
  6. data/gemfiles/rails_8.gemfile.lock +9 -1
  7. data/lib/active_storage_encryption/encrypted_blob_proxy_controller.rb +116 -0
  8. data/lib/active_storage_encryption/encrypted_blobs_controller.rb +0 -51
  9. data/lib/active_storage_encryption/encrypted_s3_service.rb +1 -0
  10. data/lib/active_storage_encryption/overrides.rb +1 -0
  11. data/lib/active_storage_encryption/private_url_policy.rb +4 -2
  12. data/lib/active_storage_encryption/version.rb +1 -1
  13. data/lib/active_storage_encryption.rb +1 -0
  14. data/test/dummy/log/development.log +304 -0
  15. data/test/dummy/log/test.log +66758 -811
  16. data/test/dummy/storage/0a/mt/0amtaps713liftrtbxt9h998epz4 +0 -0
  17. data/test/dummy/storage/0b/93/0b93pygovuunam1a3ovzwmrbuw2x +0 -0
  18. data/test/dummy/storage/0m/3s/0m3s7r3nboblijr1jxlnvm3p3l4b +0 -0
  19. data/test/dummy/storage/0o/9s/0o9s4ctbpu757qh7ucyony0itek4 +0 -0
  20. data/test/dummy/storage/1e/q6/1eq646og0wazgfw7bwjqz2uem0g4 +0 -0
  21. data/test/dummy/storage/1n/o3/1no30cpwrm727bm6arvb7zxagdg1 +0 -0
  22. data/test/dummy/storage/1x/6w/1x6wsoq3pew17reztwax78lrr3hc +0 -0
  23. data/test/dummy/storage/28/de/28deswrv89c9f2tk7dz1l5uovd4r +0 -0
  24. data/test/dummy/storage/2h/sd/2hsd1mh20c6os2nzyoicfyymhwev +0 -0
  25. data/test/dummy/storage/2t/ni/2tnidhdk4c6cj0tnw3jiw88dgs4g +0 -0
  26. data/test/dummy/storage/2v/e0/2ve0555nluisy2el5cf4txzgae3j +0 -0
  27. data/test/dummy/storage/2z/c5/2zc5mj8g0o9l7mfnim0vs4v48xd6 +0 -0
  28. data/test/dummy/storage/34/xc/34xc9hk74dm9227d6mhgfcfxl4ue +0 -0
  29. data/test/dummy/storage/3z/0t/3z0tnve7ivrq0qyrvfhfzztjhjqs +0 -0
  30. data/test/dummy/storage/49/14/4914188q1dptpw4po91cp54f32bg +0 -0
  31. data/test/dummy/storage/4c/74/4c7412lfz0pm2ocg6u01h67bnsch +0 -0
  32. data/test/dummy/storage/52/qf/52qfbgjlf3gor3agsyrt09t19o55 +0 -0
  33. data/test/dummy/storage/57/go/57gok1uc4ebc3ugrjrje4lpe1ram +0 -0
  34. data/test/dummy/storage/5f/dv/5fdvt6tu1mkyajbz4hbxbw6fpt9w +0 -0
  35. data/test/dummy/storage/5x/b7/5xb7zzi66fi5f6yrn09pq4ogb9wo +0 -0
  36. data/test/dummy/storage/6m/vr/6mvr1fr5it125tm4vahjw6bv9wkz +0 -0
  37. data/test/dummy/storage/7b/hb/7bhbdxqn67lape1f49jqfktcei4n +0 -0
  38. data/test/dummy/storage/7n/4v/7n4vpm1q14y4qffc4jj78m036gtw +0 -0
  39. data/test/dummy/storage/7q/ku/7qkufbjwbbqwnf89uciosleixnew +0 -0
  40. data/test/dummy/storage/8l/5v/8l5vb4o02hx46s5qohfn5to945p3 +0 -0
  41. data/test/dummy/storage/8q/pu/8qpun3f8vzl7auxajvqyq8f48ngw +0 -0
  42. data/test/dummy/storage/8w/ag/8wag4ptmox207h7mobamk0tcebwx +0 -0
  43. data/test/dummy/storage/8w/v8/8wv8lrhsw4s2r6guh1csd3jd89ii +0 -0
  44. data/test/dummy/storage/9b/c6/9bc6wlpfnqdywpnxgeoin3w9b5ch +0 -0
  45. data/test/dummy/storage/9l/wk/9lwkt21k5iburdaitbwliw7krtwt +0 -0
  46. data/test/dummy/storage/9p/0v/9p0vgfw3l2854k7so3rp33rmyh7p +0 -0
  47. data/test/dummy/storage/9r/sy/9rsya3r6syft34qz24g1h4u4qq44 +0 -0
  48. data/test/dummy/storage/9s/es/9seslusr46xjf3mfzq10hkp13kc1 +0 -0
  49. data/test/dummy/storage/9t/nv/9tnvn5v52fkvurpgszf4gco78t5h +0 -0
  50. data/test/dummy/storage/9u/to/9utokgxyu6xyovandu7pjhogoaqp +0 -0
  51. data/test/dummy/storage/9w/a4/9wa4c20p4dvm1cd5thnv9f7ei13w +0 -0
  52. data/test/dummy/storage/at/kg/atkgs5gwz2xdv9lvqftsg6p7gcpu +0 -0
  53. data/test/dummy/storage/at/qo/atqomgf3rpb2f6e1tq1yn2xqzojv +0 -0
  54. data/test/dummy/storage/ba/lq/balqtije6kf82ht4lr70ajaae9kc +0 -0
  55. data/test/dummy/storage/bf/i1/bfi1ij9rygr6lkx1r0lhgi8o5smx +0 -0
  56. data/test/dummy/storage/bg/ye/bgyenotrv3aj6lk88edwv0c41pfj +0 -0
  57. data/test/dummy/storage/bu/xe/buxed4b1l78kcax53fa37awm9ywk +0 -0
  58. data/test/dummy/storage/d2/c1/d2c11nhikb474oq3q7so0xbhukvj +0 -0
  59. data/test/dummy/storage/development.sqlite3 +0 -0
  60. data/test/dummy/storage/dk/hy/dkhybxn2o27a8xgvfhsxpgqxa1zf +0 -0
  61. data/test/dummy/storage/e7/2n/e72nz5cz3wf6qvh4dw4qfnw6ucog +0 -0
  62. data/test/dummy/storage/eo/4q/eo4qn68m7al0ehhe0s23ycuzkjto +0 -0
  63. data/test/dummy/storage/ew/8s/ew8sejdsx8ddmrzkvfa37ebz1ts1 +0 -0
  64. data/test/dummy/storage/f8/q1/f8q1kpg2tou8ru0afj8d2gy6ym5p +0 -0
  65. data/test/dummy/storage/fr/55/fr558uhp1k93jzhb4butyi2ry51t +0 -0
  66. data/test/dummy/storage/g4/nh/g4nhx1zxbeiegqpgn8ppsl1yhm0t +0 -0
  67. data/test/dummy/storage/gg/r5/ggr51egxhqfh4w5eluzs47qceb76 +0 -0
  68. data/test/dummy/storage/gh/ua/ghuaagralqmjy8rkbwmuv3010lvs +0 -0
  69. data/test/dummy/storage/gx/uh/gxuhmf52ufli3m7ng8irp8ghxa1v +0 -0
  70. data/test/dummy/storage/h0/m1/h0m1emy251xus1d9qh6u25dzy18o +0 -0
  71. data/test/dummy/storage/hh/kc/hhkc2q8paptyvhw2m5hlwylhtfo5 +0 -0
  72. data/test/dummy/storage/hq/0q/hq0q04kr6qzrp0qaee8rehcp2tzx +0 -0
  73. data/test/dummy/storage/ii/g1/iig1ge3fsjitai4g2fkq4qt369wh +0 -0
  74. data/test/dummy/storage/io/f0/iof0mv7w8qjd6m826g52pzyxedet +0 -0
  75. data/test/dummy/storage/jk/2i/jk2ifmx6ac35ubk3esufnm6bn1m1 +0 -0
  76. data/test/dummy/storage/jw/4t/jw4trdeyfkw3j8z70xcnr9a7gqe5 +0 -0
  77. data/test/dummy/storage/ke/k2/kek24leksglm1rs2a78mfmot0p3s +0 -0
  78. data/test/dummy/storage/kh/6d/kh6doaxxwxiyes0yqz2dmmpajkzv +0 -0
  79. data/test/dummy/storage/kj/7n/kj7nookjhisagd80z8hlv3wn50am +0 -0
  80. data/test/dummy/storage/kq/lf/kqlf5udtrgrk4v55qodxyt6i68p8 +0 -0
  81. data/test/dummy/storage/ky/33/ky334jbo8eb08pj9qbe919iz91mh +0 -0
  82. data/test/dummy/storage/lt/zw/ltzw4lur2bheit1273ogpfzhv7j1 +0 -0
  83. data/test/dummy/storage/m2/ve/m2vejmyttn1ium81dopppom6vum6 +0 -0
  84. data/test/dummy/storage/m8/d4/m8d4r9iauedq8wlpvnx1f3ou0jwg +0 -0
  85. data/test/dummy/storage/m9/ee/m9eetioklzatyff94gq0vn1cga1n +0 -0
  86. data/test/dummy/storage/ma/v0/mav084zvmyoh1a8i7dcwqy2aaoi9 +0 -0
  87. data/test/dummy/storage/mg/pa/mgpauiu02i28j3poef65k3q0gfpw +0 -0
  88. data/test/dummy/storage/mm/8g/mm8gp5evncb1ol1lj2jlmra2ixij +0 -0
  89. data/test/dummy/storage/mm/d2/mmd21x8c1amgnidzw0wowiwug4g3 +0 -0
  90. data/test/dummy/storage/n2/qr/n2qro0y9heko9cwxlf10wiqiipsw +0 -0
  91. data/test/dummy/storage/n8/b7/n8b7b7qgu6jtw577dnn10jrrmszs +0 -0
  92. data/test/dummy/storage/n8/p2/n8p2ine0qqhphn09kqtxco4y7g0a +0 -0
  93. data/test/dummy/storage/nk/vh/nkvhgk7snpdy2ak6k02htxx9swp7 +0 -0
  94. data/test/dummy/storage/nn/s0/nns0nggo0x645ytco52adnsi4myp +0 -0
  95. data/test/dummy/storage/nu/kz/nukzl7cckkzh68i7kyjkm9mzw7c0 +0 -0
  96. data/test/dummy/storage/nv/8v/nv8vyoghcde1yr1bjpsw4327qt7s +0 -0
  97. data/test/dummy/storage/of/on/ofonhf1gs26k3dpj6o7b0ktzfowh +0 -0
  98. data/test/dummy/storage/pl/pf/plpfs59hvdoogj9gdweqta36csqn +0 -0
  99. data/test/dummy/storage/q5/g5/q5g55ekmscu10pzfw6q4syigt81g +0 -0
  100. data/test/dummy/storage/q5/kc/q5kcr9twyb9v4mh31pay0t7nkuwu +0 -0
  101. data/test/dummy/storage/qa/xd/qaxdngi74r52ahqg1pz8hjddeajc +0 -0
  102. data/test/dummy/storage/r7/5v/r75vadn34ak53vinylgnfdl1s8rt +0 -0
  103. data/test/dummy/storage/rj/rg/rjrghnyzyvxpkjw1a57mrloz72x1 +0 -0
  104. data/test/dummy/storage/se/h7/seh7eorfoanpp6de62pubv7kyu1a +0 -0
  105. data/test/dummy/storage/sj/i1/sji1oj12soz2fcjcoz0gejvzo8to +0 -0
  106. data/test/dummy/storage/sn/2r/sn2rku9thay4hbcbt926an69maku +0 -0
  107. data/test/dummy/storage/sw/jm/swjmbmxou3tnarcirxc6gdycxh91 +0 -0
  108. data/test/dummy/storage/sz/mq/szmqlydvpgqaw7p3v0wh444wtcif +0 -0
  109. data/test/dummy/storage/test.sqlite3 +0 -0
  110. data/test/dummy/storage/tg/by/tgbyrdvg94ivhhy2z59e8l9fod10 +0 -0
  111. data/test/dummy/storage/u5/vm/u5vmz08tuayqggd436et8fiaeml1 +0 -0
  112. data/test/dummy/storage/u6/pf/u6pf4yky0vbmvid3fa3lm4lre68g +0 -0
  113. data/test/dummy/storage/ub/ql/ubqlmlilt8ujgdpngcm1zae41kgy +0 -0
  114. data/test/dummy/storage/un/29/un29e9khqism72ag27ojccmn5sds +0 -0
  115. data/test/dummy/storage/ux/ns/uxnsvuk4rr1p67n1oq6tmraz0gaw +0 -0
  116. data/test/dummy/storage/v1/qo/v1qor0zxg3lctk9mbwyos3oag9gj +0 -0
  117. data/test/dummy/storage/v8/ok/v8okmd7374w1obna13a7anllx2vu +0 -0
  118. data/test/dummy/storage/vd/tf/vdtfmz2ctis3dr1r35do9bow2xj5 +0 -0
  119. data/test/dummy/storage/vo/dg/vodgq1inccnujjt3auber7tt8w8o +0 -0
  120. data/test/dummy/storage/vp/oe/vpoeiq00tf9pk0jcjlccomkju1zc +0 -0
  121. data/test/dummy/storage/vu/kg/vukgoj6qf96bhealui2yaeyn4n72 +0 -0
  122. data/test/dummy/storage/w7/2z/w72zoqu7v6v6jp0tpy671dcbvpow +0 -0
  123. data/test/dummy/storage/wa/3f/wa3fncsnozc6n4xfu32gw34geqcd +0 -0
  124. data/test/dummy/storage/wy/ix/wyixbqx3f6a4agb8bjhrtpblpaua +0 -0
  125. data/test/dummy/storage/xd/st/xdsttma3tqt7mex0vhp1vsm3dq16 +0 -0
  126. data/test/dummy/storage/xv/ej/xvejm2e064bnpunx3nmktaqs0x90 +0 -0
  127. data/test/dummy/storage/xx/py/xxpyyodssq2xmp57qrtvuw0wchwk +0 -0
  128. data/test/dummy/storage/xz/ik/xzikejc5sohi3zexa93s9xmg4jst +0 -0
  129. data/test/dummy/storage/y4/g8/y4g8teo86blcv0zysa2d2jawvk6i +0 -0
  130. data/test/dummy/storage/y9/58/y958xli6aoktx1ehuyjc1k8dcbzv +0 -0
  131. data/test/dummy/storage/yj/lw/yjlw8bf70iujb16deja8ae43rqbc +0 -0
  132. data/test/dummy/storage/z3/qy/z3qyb9avbucwhxa8909rpfued0y5 +0 -0
  133. data/test/dummy/storage/zr/wu/zrwudcg4kgo7r0jemszuzok8grqp +0 -0
  134. data/test/dummy/tmp/local_secret.txt +1 -0
  135. data/test/integration/encrypted_blob_proxy_controller_test.rb +253 -0
  136. data/test/integration/encrypted_blobs_controller_test.rb +0 -130
  137. data/test/lib/encrypted_disk_service_test.rb +5 -119
  138. data/test/lib/encrypted_mirror_service_test.rb +1 -1
  139. data/test/lib/encrypted_s3_service_test.rb +5 -2
  140. metadata +137 -4
  141. data/test/dummy/storage/x6/pl/x6plznfuhrsyjn9pox2a6xgmcs3x +0 -0
  142. data/test/dummy/storage/yq/sv/yqsvw5a72b3fv719zq8a6yb7lv0j +0 -0
Binary file
@@ -0,0 +1 @@
1
+ 618685350e7e01cbbeeb70c27ec07e8b83919c4fa7fac6831a0f1f8a13d2d2a03e2088e434515ddf25dfe4c62c21efd22410011d56678caf826e27d7b365d24a
@@ -0,0 +1,253 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class ActiveStorageEncryptionEncryptedBlobProxyControllerTest < ActionDispatch::IntegrationTest
6
+ setup do
7
+ @storage_dir = Dir.mktmpdir
8
+ @other_storage_dir = Dir.mktmpdir
9
+ @service = ActiveStorageEncryption::EncryptedDiskService.new(root: @storage_dir, private_url_policy: "stream")
10
+ @service.name = "amazing_encrypting_disk_service" # Needed for the controller and service lookup
11
+
12
+ # Hack: sneakily add our service to them configurations
13
+ # ActiveStorage::Blob.services.send(:services)["amazing_encrypting_disk_service"] = @service
14
+
15
+ # We need to set our service as the default, because the controller does lookup from the application config -
16
+ # which does not include the service we define here
17
+ @previous_default_service = ActiveStorage::Blob.service
18
+ @previous_services = ActiveStorage::Blob.services
19
+
20
+ # To catch potential issues where something goes to the default service by mistake, let's set a
21
+ # different Service as the default
22
+ @non_encrypted_default_service = ActiveStorage::Service::DiskService.new(root: @other_storage_dir)
23
+ ActiveStorage::Blob.service = @non_encrypted_default_service
24
+ ActiveStorage::Blob.services = {@service.name => @service} # That too
25
+
26
+ # This needs to be set
27
+ ActiveStorageEncryption::Engine.routes.default_url_options = {host: "www.example.com"}
28
+
29
+ # We need to use a hostname for ActiveStorage which is in the Rails authorized hosts.
30
+ # see https://stackoverflow.com/a/60573259/153886
31
+ ActiveStorage::Current.url_options = {
32
+ host: "www.example.com",
33
+ protocol: "https"
34
+ }
35
+ freeze_time # For testing expiring tokens
36
+ https! # So that all requests are simulated as SSL
37
+ end
38
+
39
+ def teardown
40
+ unfreeze_time
41
+ ActiveStorage::Blob.service = @previous_default_service
42
+ ActiveStorage::Blob.services = @previous_services
43
+ FileUtils.rm_rf(@storage_dir)
44
+ FileUtils.rm_rf(@other_storage_dir)
45
+ end
46
+
47
+ def engine_routes
48
+ ActiveStorageEncryption::Engine.routes.url_helpers
49
+ end
50
+
51
+ test "show() serves the complete decrypted blob body" do
52
+ rng = Random.new(Minitest.seed)
53
+ plaintext = rng.bytes(512)
54
+
55
+ blob = ActiveStorage::Blob.create_and_upload!(io: StringIO.new(plaintext), content_type: "x-office/severance", filename: "secret.bin", service_name: @service.name)
56
+ assert blob.encryption_key
57
+
58
+ streaming_url = blob.url(disposition: "inline") # This generates a URL with the byte size
59
+ get streaming_url
60
+
61
+ assert_response :success
62
+ assert_equal "x-office/severance", response.headers["content-type"]
63
+ assert_equal blob.key.inspect, response.headers["etag"]
64
+ assert_equal plaintext, response.body
65
+ end
66
+
67
+ test "show() serves a blob of 0 size" do
68
+ Random.new(Minitest.seed)
69
+ plaintext = "".b
70
+
71
+ blob = ActiveStorage::Blob.create_and_upload!(io: StringIO.new(plaintext), content_type: "x-office/severance", filename: "secret.bin", service_name: @service.name)
72
+ assert blob.encryption_key
73
+
74
+ streaming_url = blob.url(disposition: "inline") # This generates a URL with the byte size
75
+ get streaming_url
76
+
77
+ assert_response :success
78
+ assert response.body.empty?
79
+ end
80
+
81
+ test "show() returns a 404 when the blob no longer exists on the service" do
82
+ Random.new(Minitest.seed)
83
+ plaintext = "hello"
84
+
85
+ blob = ActiveStorage::Blob.create_and_upload!(io: StringIO.new(plaintext), content_type: "x-office/severance", filename: "secret.bin", service_name: @service.name)
86
+ assert blob.encryption_key
87
+
88
+ streaming_url = blob.url(disposition: "inline") # This generates a URL with the byte size
89
+ blob.service.delete(blob.key)
90
+
91
+ get streaming_url
92
+
93
+ assert_response :not_found
94
+ end
95
+
96
+ test "show() serves HTTP ranges" do
97
+ rng = Random.new(Minitest.seed)
98
+ plaintext = rng.bytes(5.megabytes + 13)
99
+
100
+ blob = ActiveStorage::Blob.create_and_upload!(io: StringIO.new(plaintext), content_type: "x-office/severance", filename: "secret.bin", service_name: @service.name)
101
+ assert blob.encryption_key
102
+
103
+ streaming_url = blob.url(disposition: "inline") # This generates a URL with the byte size
104
+ get streaming_url, headers: {"Range" => "bytes=0-0"}
105
+
106
+ assert_response :partial_content
107
+ assert_equal "1", response.headers["content-length"]
108
+ assert_equal "bytes 0-0/5242893", response.headers["content-range"]
109
+ assert_equal "x-office/severance", response.headers["content-type"]
110
+ assert_equal plaintext[0..0], response.body
111
+
112
+ get streaming_url, headers: {"Range" => "bytes=1-2"}
113
+
114
+ assert_response :partial_content
115
+ assert_equal "2", response.headers["content-length"]
116
+ assert_equal "bytes 1-2/5242893", response.headers["content-range"]
117
+ assert_equal "x-office/severance", response.headers["content-type"]
118
+ assert_equal plaintext[1..2], response.body
119
+
120
+ get streaming_url, headers: {"Range" => "bytes=1-2,8-10,12-23"}
121
+
122
+ assert_response :partial_content
123
+ assert response.headers["content-type"].start_with?("multipart/byteranges; boundary=")
124
+ assert_nil response.headers["content-range"]
125
+ assert_equal 350, response.body.bytesize
126
+
127
+ get streaming_url, headers: {"Range" => "bytes=99999999999999999-99999999999999999"}
128
+ assert_response :range_not_satisfiable
129
+ end
130
+
131
+ test "show() refuses a request which goes to a non-encrypted Service" do
132
+ rng = Random.new(Minitest.seed)
133
+
134
+ key = SecureRandom.base36(12)
135
+ encryption_key = rng.bytes(32)
136
+ plaintext = rng.bytes(512)
137
+ @service.upload(key, StringIO.new(plaintext).binmode, encryption_key: encryption_key)
138
+
139
+ streaming_url = @service.url(key, encryption_key: encryption_key, filename: ActiveStorage::Filename.new("private.doc"),
140
+ expires_in: 30.seconds, disposition: "inline", content_type: "x-office/severance",
141
+ blob_byte_size: plaintext.bytesize)
142
+
143
+ # Sneak in a non-encrypted service under the same key
144
+ ActiveStorage::Blob.services[@service.name] = @non_encrypted_default_service
145
+
146
+ get streaming_url
147
+ assert_response :forbidden
148
+ end
149
+
150
+ test "show() refuses a request which has an incorrect encryption key" do
151
+ rng = Random.new(Minitest.seed)
152
+
153
+ key = SecureRandom.base36(12)
154
+ encryption_key = rng.bytes(32)
155
+ plaintext = rng.bytes(512)
156
+ @service.upload(key, StringIO.new(plaintext).binmode, encryption_key: encryption_key)
157
+
158
+ another_encryption_key = rng.bytes(32)
159
+ refute_equal encryption_key, another_encryption_key
160
+
161
+ streaming_url = @service.url(key, encryption_key: another_encryption_key,
162
+ filename: ActiveStorage::Filename.new("private.doc"), expires_in: 30.seconds,
163
+ disposition: "inline", content_type: "x-office/severance", blob_byte_size: plaintext.bytesize)
164
+ get streaming_url
165
+
166
+ assert_response :forbidden
167
+ end
168
+
169
+ test "show() refuses a request with a garbage token" do
170
+ get engine_routes.encrypted_blob_streaming_get_path(token: "garbage", filename: "exfil.bin")
171
+ assert_response :forbidden
172
+ end
173
+
174
+ test "show() refuses a request with a token that has been encrypted using an incorrect encryption key" do
175
+ https!
176
+ rng = Random.new(Minitest.seed)
177
+ encryptor_key = rng.bytes(32)
178
+ other_encryptor = ActiveStorageEncryption::TokenEncryptor.new(encryptor_key, url_safe: encryptor_key)
179
+
180
+ key = SecureRandom.base36(12)
181
+ encryption_key = rng.bytes(32)
182
+ @service.upload(key, StringIO.new(rng.bytes(512)).binmode, encryption_key: encryption_key)
183
+
184
+ streaming_url = ActiveStorageEncryption.stub(:token_encryptor, -> { other_encryptor }) do
185
+ @service.url(key, encryption_key: encryption_key,
186
+ filename: ActiveStorage::Filename.new("private.doc"), expires_in: 3.seconds,
187
+ disposition: "inline", content_type: "binary/octet-stream",
188
+ blob_byte_size: 512)
189
+ end
190
+
191
+ get streaming_url
192
+ assert_response :forbidden
193
+ end
194
+
195
+ test "show() refuses a request with a token that has expired" do
196
+ rng = Random.new(Minitest.seed)
197
+
198
+ key = SecureRandom.base36(12)
199
+ encryption_key = rng.bytes(32)
200
+ @service.upload(key, StringIO.new(rng.bytes(512)).binmode, encryption_key: encryption_key)
201
+
202
+ streaming_url = @service.url(key, encryption_key: encryption_key,
203
+ filename: ActiveStorage::Filename.new("private.doc"), expires_in: 3.seconds,
204
+ disposition: "inline", content_type: "binary/octet-stream",
205
+ blob_byte_size: 512)
206
+ travel 5.seconds
207
+
208
+ get streaming_url
209
+ assert_response :forbidden
210
+ end
211
+
212
+ test "show() requires headers if the private_url_policy of the service is set to :require_headers" do
213
+ rng = Random.new(Minitest.seed)
214
+
215
+ key = SecureRandom.base36(12)
216
+ encryption_key = rng.bytes(32)
217
+ plaintext = rng.bytes(512)
218
+ @service.upload(key, StringIO.new(plaintext).binmode, encryption_key: encryption_key)
219
+
220
+ # The policy needs to be set before we generate the token (the token includes require_headers)
221
+ @service.private_url_policy = :require_headers
222
+ streaming_url = @service.url(key, encryption_key: encryption_key,
223
+ filename: ActiveStorage::Filename.new("private.doc"), expires_in: 30.seconds, disposition: "inline",
224
+ content_type: "x-office/severance", blob_byte_size: plaintext.bytesize)
225
+
226
+ get streaming_url
227
+ assert_response :forbidden # Without headers
228
+
229
+ get streaming_url, headers: {"HTTP_X_ACTIVE_STORAGE_ENCRYPTION_KEY" => Base64.strict_encode64(encryption_key)}
230
+ assert_response :success
231
+ assert_equal "x-office/severance", response.headers["content-type"]
232
+ assert_equal plaintext, response.body
233
+ end
234
+
235
+ test "show() refuses a request if the service no longer permits private URLs, even if the URL was generated when it used to permit them" do
236
+ rng = Random.new(Minitest.seed)
237
+
238
+ SecureRandom.base36(12)
239
+ plaintext = rng.bytes(512)
240
+
241
+ blob = ActiveStorage::Blob.create_and_upload!(io: StringIO.new(plaintext), content_type: "x-office/severance", filename: "secret.bin", service_name: @service.name)
242
+ assert blob.encryption_key
243
+ streaming_url = blob.url(disposition: "inline", content_type: "x-office/severance")
244
+
245
+ @service.private_url_policy = :disable
246
+
247
+ get streaming_url
248
+ assert_response :forbidden # Without headers
249
+
250
+ get streaming_url, headers: {"HTTP_X_ACTIVE_STORAGE_ENCRYPTION_KEY" => Base64.strict_encode64(blob.encryption_key)}
251
+ assert_response :forbidden # With headers
252
+ end
253
+ end