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
@@ -9,9 +9,6 @@ class ActiveStorageEncryptionEncryptedBlobsControllerTest < ActionDispatch::Inte
9
9
  @service = ActiveStorageEncryption::EncryptedDiskService.new(root: @storage_dir, private_url_policy: "stream")
10
10
  @service.name = "amazing_encrypting_disk_service" # Needed for the controller and service lookup
11
11
 
12
- # Hack: sneakily add our service to them configurations
13
- # ActiveStorage::Blob.services.send(:services)["amazing_encrypting_disk_service"] = @service
14
-
15
12
  # We need to set our service as the default, because the controller does lookup from the application config -
16
13
  # which does not include the service we define here
17
14
  @previous_default_service = ActiveStorage::Blob.service
@@ -48,133 +45,6 @@ class ActiveStorageEncryptionEncryptedBlobsControllerTest < ActionDispatch::Inte
48
45
  ActiveStorageEncryption::Engine.routes.url_helpers
49
46
  end
50
47
 
51
- test "show() returns the decrypted blob body" do
52
- rng = Random.new(Minitest.seed)
53
-
54
- key = SecureRandom.base36(12)
55
- encryption_key = rng.bytes(32)
56
- plaintext = rng.bytes(512)
57
- @service.upload(key, StringIO.new(plaintext).binmode, encryption_key: encryption_key)
58
-
59
- streaming_url = @service.url(key, encryption_key: encryption_key, filename: ActiveStorage::Filename.new("private.doc"), expires_in: 30.seconds, disposition: "inline", content_type: "x-office/severance")
60
- get streaming_url
61
-
62
- assert_response :success
63
- assert_equal "x-office/severance", response.headers["content-type"]
64
- assert_equal plaintext, response.body
65
- end
66
-
67
- test "show() refuses a request which goes to a non-encrypted Service" do
68
- rng = Random.new(Minitest.seed)
69
-
70
- key = SecureRandom.base36(12)
71
- encryption_key = rng.bytes(32)
72
- plaintext = rng.bytes(512)
73
- @service.upload(key, StringIO.new(plaintext).binmode, encryption_key: encryption_key)
74
-
75
- streaming_url = @service.url(key, encryption_key: encryption_key, filename: ActiveStorage::Filename.new("private.doc"), expires_in: 30.seconds, disposition: "inline", content_type: "x-office/severance")
76
-
77
- # Sneak in a non-encrypted service under the same key
78
- ActiveStorage::Blob.services[@service.name] = @non_encrypted_default_service
79
-
80
- get streaming_url
81
- assert_response :forbidden
82
- end
83
-
84
- test "show() refuses a request which has an incorrect encryption key" do
85
- rng = Random.new(Minitest.seed)
86
-
87
- key = SecureRandom.base36(12)
88
- encryption_key = rng.bytes(32)
89
- plaintext = rng.bytes(512)
90
- @service.upload(key, StringIO.new(plaintext).binmode, encryption_key: encryption_key)
91
-
92
- another_encryption_key = rng.bytes(32)
93
- refute_equal encryption_key, another_encryption_key
94
-
95
- streaming_url = @service.url(key, encryption_key: another_encryption_key, filename: ActiveStorage::Filename.new("private.doc"), expires_in: 30.seconds, disposition: "inline", content_type: "x-office/severance")
96
- get streaming_url
97
-
98
- assert_response :forbidden
99
- end
100
-
101
- test "show() refuses a request with a garbage token" do
102
- get engine_routes.encrypted_blob_streaming_get_path(token: "garbage", filename: "exfil.bin")
103
- assert_response :forbidden
104
- end
105
-
106
- test "show() refuses a request with a token that has been encrypted using an incorrect encryption key" do
107
- https!
108
- rng = Random.new(Minitest.seed)
109
- encryptor_key = rng.bytes(32)
110
- other_encryptor = ActiveStorageEncryption::TokenEncryptor.new(encryptor_key, url_safe: encryptor_key)
111
-
112
- key = SecureRandom.base36(12)
113
- encryption_key = rng.bytes(32)
114
- @service.upload(key, StringIO.new(rng.bytes(512)).binmode, encryption_key: encryption_key)
115
-
116
- streaming_url = ActiveStorageEncryption.stub(:token_encryptor, -> { other_encryptor }) do
117
- @service.url(key, encryption_key: encryption_key, filename: ActiveStorage::Filename.new("private.doc"), expires_in: 3.seconds, disposition: "inline", content_type: "binary/octet-stream")
118
- end
119
-
120
- get streaming_url
121
- assert_response :forbidden
122
- end
123
-
124
- test "show() refuses a request with a token that has expired" do
125
- rng = Random.new(Minitest.seed)
126
-
127
- key = SecureRandom.base36(12)
128
- encryption_key = rng.bytes(32)
129
- @service.upload(key, StringIO.new(rng.bytes(512)).binmode, encryption_key: encryption_key)
130
-
131
- streaming_url = @service.url(key, encryption_key: encryption_key, filename: ActiveStorage::Filename.new("private.doc"), expires_in: 3.seconds, disposition: "inline", content_type: "binary/octet-stream")
132
- travel 5.seconds
133
-
134
- get streaming_url
135
- assert_response :forbidden
136
- end
137
-
138
- test "show() requires headers if the private_url_policy of the service is set to :require_headers" do
139
- rng = Random.new(Minitest.seed)
140
-
141
- key = SecureRandom.base36(12)
142
- encryption_key = rng.bytes(32)
143
- plaintext = rng.bytes(512)
144
- @service.upload(key, StringIO.new(plaintext).binmode, encryption_key: encryption_key)
145
-
146
- # The policy needs to be set before we generate the token (the token includes require_headers)
147
- @service.private_url_policy = :require_headers
148
- streaming_url = @service.url(key, encryption_key: encryption_key, filename: ActiveStorage::Filename.new("private.doc"), expires_in: 30.seconds, disposition: "inline", content_type: "x-office/severance")
149
-
150
- get streaming_url
151
- assert_response :forbidden # Without headers
152
-
153
- get streaming_url, headers: {"HTTP_X_ACTIVE_STORAGE_ENCRYPTION_KEY" => Base64.strict_encode64(encryption_key)}
154
- assert_response :success
155
- assert_equal "x-office/severance", response.headers["content-type"]
156
- assert_equal plaintext, response.body
157
- end
158
-
159
- test "show() refuses a request if the service no longer permits private URLs" do
160
- rng = Random.new(Minitest.seed)
161
-
162
- key = SecureRandom.base36(12)
163
- encryption_key = rng.bytes(32)
164
- plaintext = rng.bytes(512)
165
- @service.upload(key, StringIO.new(plaintext).binmode, encryption_key: encryption_key)
166
-
167
- streaming_url = @service.url(key, encryption_key: encryption_key, filename: ActiveStorage::Filename.new("private.doc"), expires_in: 30.seconds, disposition: "inline", content_type: "x-office/severance")
168
-
169
- @service.private_url_policy = :disable
170
-
171
- get streaming_url
172
- assert_response :forbidden # Without headers
173
-
174
- get streaming_url, headers: {"HTTP_X_ACTIVE_STORAGE_ENCRYPTION_KEY" => Base64.strict_encode64(encryption_key)}
175
- assert_response :forbidden # Without headers
176
- end
177
-
178
48
  test "create_direct_upload creates a blob and returns the headers and the URL to start the upload, which are for the correct service name" do
179
49
  rng = Random.new(Minitest.seed)
180
50
  plaintext = rng.bytes(512)
@@ -91,95 +91,15 @@ class ActiveStorageEncryption::EncryptedDiskServiceTest < ActiveSupport::TestCas
91
91
  assert_equal Digest::SHA256.hexdigest(plaintext_upload_bytes), Digest::SHA256.hexdigest(readback_bytes)
92
92
  end
93
93
 
94
- def test_get_with_headers_always_succeeds
95
- @service.private_url_policy = :require_headers
96
-
97
- key = "key-1"
98
- k = Random.bytes(68)
99
- plaintext_upload_bytes = generate_random_binary_string
100
- @service.upload(key, StringIO.new(plaintext_upload_bytes), encryption_key: k)
101
-
102
- ActiveStorage::Blob.service = @service # So that the controller can find it
103
-
104
- # ActiveStorage wraps the passed filename in a wrapper thingy
105
- filename_with_sanitization = ActiveStorage::Filename.new("temp.bin")
106
- url = @service.url(key, filename: filename_with_sanitization, content_type: "binary/octet-stream", disposition: "inline", encryption_key: k, expires_in: 10.seconds)
107
- assert url.include?("/active-storage-encryption/blob/")
108
-
109
- uri = URI.parse(url)
110
-
111
- # Do a super-minimalistic test on the DiskController. ActionController is actually a Rack app (or, rather: every controller action is a Rack app).
112
- # It can thus be called with a minimal Rack env. For the definition of "minimal", see https://github.com/rack/rack/blob/main/SPEC.rdoc#the-environment-
113
- rack_env = {
114
- "SCRIPT_NAME" => "",
115
- "PATH_INFO" => uri.path,
116
- "QUERY_STRING" => uri.query,
117
- "REQUEST_METHOD" => "GET",
118
- "SERVER_NAME" => uri.host,
119
- "HTTP_X_ACTIVE_STORAGE_ENCRYPTION_KEY" => Base64.strict_encode64(k),
120
- "rack.input" => StringIO.new(""),
121
- "action_dispatch.request.parameters" => {
122
- # The controller expects the Rails router to have injected this param by extracting
123
- # it from the route path. The upload param is mapped to :encoded_token, the download param is
124
- # mapped to :encoded_key - likely because there was an exploit with ActiveStorage where keys
125
- # generated for download could be used for uploading (and thus - overwriting)
126
- "token" => uri.path.split("/")[-2] # For "show", the last path param is actually the filename - this is because Content-Disposition can be unreliable for download filename
127
- }
128
- }
129
- action_app = ActiveStorageEncryption::EncryptedBlobsController.action(:show)
130
- status, _headers, body = action_app.call(rack_env)
131
-
132
- assert_equal 200, status
133
-
134
- readback_bytes = (+"").b.tap do |buf|
135
- body.each { |chunk| buf << chunk }
136
- end
137
- assert_equal Digest::SHA256.hexdigest(plaintext_upload_bytes), Digest::SHA256.hexdigest(readback_bytes)
138
- end
139
-
140
- def test_get_without_headers_succeeds_if_service_permits
94
+ def test_private_url
141
95
  @service.private_url_policy = :stream
142
96
 
143
- key = "key-1"
144
- k = Random.bytes(68)
145
- plaintext_upload_bytes = generate_random_binary_string
146
- @service.upload(key, StringIO.new(plaintext_upload_bytes), encryption_key: k)
147
-
148
- ActiveStorage::Blob.service = @service # So that the controller can find it
149
-
150
97
  # ActiveStorage wraps the passed filename in a wrapper thingy
151
98
  filename_with_sanitization = ActiveStorage::Filename.new("temp.bin")
152
- url = @service.url(key, filename: filename_with_sanitization, content_type: "binary/octet-stream", disposition: "inline", encryption_key: k, expires_in: 10.seconds)
99
+ key = "key-1"
100
+ encryption_key = Random.bytes(32)
101
+ url = @service.url(key, blob_byte_size: 14, filename: filename_with_sanitization, content_type: "binary/octet-stream", disposition: "inline", encryption_key:, expires_in: 10.seconds)
153
102
  assert url.include?("/active-storage-encryption/blob/")
154
-
155
- uri = URI.parse(url)
156
-
157
- # Do a super-minimalistic test on the DiskController. ActionController is actually a Rack app (or, rather: every controller action is a Rack app).
158
- # It can thus be called with a minimal Rack env. For the definition of "minimal", see https://github.com/rack/rack/blob/main/SPEC.rdoc#the-environment-
159
- rack_env = {
160
- "SCRIPT_NAME" => "",
161
- "PATH_INFO" => uri.path,
162
- "QUERY_STRING" => uri.query,
163
- "REQUEST_METHOD" => "GET",
164
- "SERVER_NAME" => uri.host,
165
- "rack.input" => StringIO.new(""),
166
- "action_dispatch.request.parameters" => {
167
- # The controller expects the Rails router to have injected this param by extracting
168
- # it from the route path. The upload param is mapped to :encoded_token, the download param is
169
- # mapped to :encoded_key - likely because there was an exploit with ActiveStorage where keys
170
- # generated for download could be used for uploading (and thus - overwriting)
171
- "token" => uri.path.split("/")[-2] # For "show", the last path param is actually the filename - this is because Content-Disposition can be unreliable for download filename
172
- }
173
- }
174
- action_app = ActiveStorageEncryption::EncryptedBlobsController.action(:show)
175
- status, _headers, body = action_app.call(rack_env)
176
-
177
- assert_equal 200, status
178
-
179
- readback_bytes = (+"").b.tap do |buf|
180
- body.each { |chunk| buf << chunk }
181
- end
182
- assert_equal Digest::SHA256.hexdigest(plaintext_upload_bytes), Digest::SHA256.hexdigest(readback_bytes)
183
103
  end
184
104
 
185
105
  def test_generating_url_fails_if_streaming_is_off_for_the_service
@@ -193,44 +113,10 @@ class ActiveStorageEncryption::EncryptedDiskServiceTest < ActiveSupport::TestCas
193
113
  # ActiveStorage wraps the passed filename in a wrapper thingy
194
114
  filename_with_sanitization = ActiveStorage::Filename.new("temp.bin")
195
115
  assert_raises ActiveStorageEncryption::StreamingDisabled do
196
- @service.url(key, filename: filename_with_sanitization, content_type: "binary/octet-stream", disposition: "inline", encryption_key: k, expires_in: 10.seconds)
116
+ @service.url(key, blob_byte_size: 12, filename: filename_with_sanitization, content_type: "binary/octet-stream", disposition: "inline", encryption_key: k, expires_in: 10.seconds)
197
117
  end
198
118
  end
199
119
 
200
- def test_get_without_headers_fails_if_service_does_not_permit
201
- @service.private_url_policy = :require_headers
202
-
203
- key = "key-1"
204
- k = Random.bytes(68)
205
- plaintext_upload_bytes = generate_random_binary_string
206
- @service.upload(key, StringIO.new(plaintext_upload_bytes), encryption_key: k)
207
-
208
- ActiveStorage::Blob.service = @service # So that the controller can find it
209
-
210
- # ActiveStorage wraps the passed filename in a wrapper thingy
211
- filename_with_sanitization = ActiveStorage::Filename.new("temp.bin")
212
- url = @service.url(key, filename: filename_with_sanitization, content_type: "binary/octet-stream", disposition: "inline", encryption_key: k, expires_in: 10.seconds)
213
- uri = URI.parse(url)
214
-
215
- # Do a super-minimalistic test on the DiskController. ActionController is actually a Rack app (or, rather: every controller action is a Rack app).
216
- # It can thus be called with a minimal Rack env. For the definition of "minimal", see https://github.com/rack/rack/blob/main/SPEC.rdoc#the-environment-
217
- rack_env = {
218
- "SCRIPT_NAME" => "",
219
- "PATH_INFO" => uri.path,
220
- "QUERY_STRING" => uri.query,
221
- "REQUEST_METHOD" => "GET",
222
- "SERVER_NAME" => uri.host,
223
- "rack.input" => StringIO.new(""),
224
- # Omit x-disk-encryption-key
225
- "action_dispatch.request.parameters" => {
226
- "token" => uri.path.split("/")[-2] # For "show", the last path param is actually the filename - this is because Content-Disposition can be unreliable for download filename
227
- }
228
- }
229
- action_app = ActiveStorageEncryption::EncryptedBlobsController.action(:show)
230
- status, _headers, _body = action_app.call(rack_env)
231
- assert_equal 403, status
232
- end
233
-
234
120
  def test_upload_then_download_using_correct_key
235
121
  storage_blob_key = "key-1"
236
122
  k = Random.bytes(68)
@@ -119,7 +119,7 @@ class ActiveStorageEncryption::EncryptedMirrorServiceTest < ActiveSupport::TestC
119
119
 
120
120
  # ActiveStorage wraps the passed filename in a wrapper thingy
121
121
  filename_with_sanitization = ActiveStorage::Filename.new("temp.bin")
122
- url = @service.url(key, filename: filename_with_sanitization, content_type: "binary/octet-stream", disposition: "inline", encryption_key: k, expires_in: 10.seconds)
122
+ url = @service.url(key, blob_byte_size: 13, filename: filename_with_sanitization, content_type: "binary/octet-stream", disposition: "inline", encryption_key: k, expires_in: 10.seconds)
123
123
  assert url.include?("/active-storage-encryption/blob/")
124
124
  end
125
125
 
@@ -73,7 +73,9 @@ class ActiveStorageEncryption::EncryptedS3ServiceTest < ActiveSupport::TestCase
73
73
 
74
74
  # ActiveStorage wraps the passed filename in a wrapper thingy
75
75
  filename_with_sanitization = ActiveStorage::Filename.new("temp.bin")
76
- url = @service.url(key, filename: filename_with_sanitization, content_type: "binary/octet-stream", disposition: "inline", encryption_key: k, expires_in: 10.seconds)
76
+ url = @service.url(key, blob_byte_size: plaintext_upload_bytes.bytesize,
77
+ filename: filename_with_sanitization, content_type: "binary/octet-stream",
78
+ disposition: "inline", encryption_key: k, expires_in: 10.seconds)
77
79
  assert url.include?("/active-storage-encryption/blob/")
78
80
  end
79
81
 
@@ -88,7 +90,8 @@ class ActiveStorageEncryption::EncryptedS3ServiceTest < ActiveSupport::TestCase
88
90
 
89
91
  # ActiveStorage wraps the passed filename in a wrapper thingy
90
92
  filename_with_sanitization = ActiveStorage::Filename.new("temp.bin")
91
- url = @service.url(key, filename: filename_with_sanitization, content_type: "binary/octet-stream", disposition: "inline", encryption_key: k, expires_in: 240.seconds)
93
+ url = @service.url(key, blob_byte_size: plaintext_upload_bytes.bytesize,
94
+ filename: filename_with_sanitization, content_type: "binary/octet-stream", disposition: "inline", encryption_key: k, expires_in: 240.seconds)
92
95
 
93
96
  assert url.include?("x-amz-server-side-encryption-customer-algorithm")
94
97
  refute url.include?("x-amz-server-side-encryption-customer-key=") # The key should not be in the URL
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_storage_encryption
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Julik Tarkhanov
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2025-03-12 00:00:00.000000000 Z
12
+ date: 2025-03-27 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -39,6 +39,20 @@ dependencies:
39
39
  - - ">="
40
40
  - !ruby/object:Gem::Version
41
41
  version: 0.0.4
42
+ - !ruby/object:Gem::Dependency
43
+ name: serve_byte_range
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '1.0'
49
+ type: :runtime
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '1.0'
42
56
  - !ruby/object:Gem::Dependency
43
57
  name: activestorage
44
58
  requirement: !ruby/object:Gem::Requirement
@@ -174,6 +188,7 @@ files:
174
188
  - lib/active_storage/service/encrypted_mirror_service.rb
175
189
  - lib/active_storage/service/encrypted_s3_service.rb
176
190
  - lib/active_storage_encryption.rb
191
+ - lib/active_storage_encryption/encrypted_blob_proxy_controller.rb
177
192
  - lib/active_storage_encryption/encrypted_blobs_controller.rb
178
193
  - lib/active_storage_encryption/encrypted_disk_service.rb
179
194
  - lib/active_storage_encryption/encrypted_disk_service/v1_scheme.rb
@@ -219,6 +234,7 @@ files:
219
234
  - test/dummy/db/migrate/20250304023851_create_active_storage_tables.active_storage.rb
220
235
  - test/dummy/db/migrate/20250304023853_add_blob_encryption_key_column.rb
221
236
  - test/dummy/db/schema.rb
237
+ - test/dummy/log/development.log
222
238
  - test/dummy/log/test.log
223
239
  - test/dummy/public/404.html
224
240
  - test/dummy/public/406-unsupported-browser.html
@@ -226,9 +242,126 @@ files:
226
242
  - test/dummy/public/500.html
227
243
  - test/dummy/public/icon.png
228
244
  - test/dummy/public/icon.svg
245
+ - test/dummy/storage/0a/mt/0amtaps713liftrtbxt9h998epz4
246
+ - test/dummy/storage/0b/93/0b93pygovuunam1a3ovzwmrbuw2x
247
+ - test/dummy/storage/0m/3s/0m3s7r3nboblijr1jxlnvm3p3l4b
248
+ - test/dummy/storage/0o/9s/0o9s4ctbpu757qh7ucyony0itek4
249
+ - test/dummy/storage/1e/q6/1eq646og0wazgfw7bwjqz2uem0g4
250
+ - test/dummy/storage/1n/o3/1no30cpwrm727bm6arvb7zxagdg1
251
+ - test/dummy/storage/1x/6w/1x6wsoq3pew17reztwax78lrr3hc
252
+ - test/dummy/storage/28/de/28deswrv89c9f2tk7dz1l5uovd4r
253
+ - test/dummy/storage/2h/sd/2hsd1mh20c6os2nzyoicfyymhwev
254
+ - test/dummy/storage/2t/ni/2tnidhdk4c6cj0tnw3jiw88dgs4g
255
+ - test/dummy/storage/2v/e0/2ve0555nluisy2el5cf4txzgae3j
256
+ - test/dummy/storage/2z/c5/2zc5mj8g0o9l7mfnim0vs4v48xd6
257
+ - test/dummy/storage/34/xc/34xc9hk74dm9227d6mhgfcfxl4ue
258
+ - test/dummy/storage/3z/0t/3z0tnve7ivrq0qyrvfhfzztjhjqs
259
+ - test/dummy/storage/49/14/4914188q1dptpw4po91cp54f32bg
260
+ - test/dummy/storage/4c/74/4c7412lfz0pm2ocg6u01h67bnsch
261
+ - test/dummy/storage/52/qf/52qfbgjlf3gor3agsyrt09t19o55
262
+ - test/dummy/storage/57/go/57gok1uc4ebc3ugrjrje4lpe1ram
263
+ - test/dummy/storage/5f/dv/5fdvt6tu1mkyajbz4hbxbw6fpt9w
264
+ - test/dummy/storage/5x/b7/5xb7zzi66fi5f6yrn09pq4ogb9wo
265
+ - test/dummy/storage/6m/vr/6mvr1fr5it125tm4vahjw6bv9wkz
266
+ - test/dummy/storage/7b/hb/7bhbdxqn67lape1f49jqfktcei4n
267
+ - test/dummy/storage/7n/4v/7n4vpm1q14y4qffc4jj78m036gtw
268
+ - test/dummy/storage/7q/ku/7qkufbjwbbqwnf89uciosleixnew
269
+ - test/dummy/storage/8l/5v/8l5vb4o02hx46s5qohfn5to945p3
270
+ - test/dummy/storage/8q/pu/8qpun3f8vzl7auxajvqyq8f48ngw
271
+ - test/dummy/storage/8w/ag/8wag4ptmox207h7mobamk0tcebwx
272
+ - test/dummy/storage/8w/v8/8wv8lrhsw4s2r6guh1csd3jd89ii
273
+ - test/dummy/storage/9b/c6/9bc6wlpfnqdywpnxgeoin3w9b5ch
274
+ - test/dummy/storage/9l/wk/9lwkt21k5iburdaitbwliw7krtwt
275
+ - test/dummy/storage/9p/0v/9p0vgfw3l2854k7so3rp33rmyh7p
276
+ - test/dummy/storage/9r/sy/9rsya3r6syft34qz24g1h4u4qq44
277
+ - test/dummy/storage/9s/es/9seslusr46xjf3mfzq10hkp13kc1
278
+ - test/dummy/storage/9t/nv/9tnvn5v52fkvurpgszf4gco78t5h
279
+ - test/dummy/storage/9u/to/9utokgxyu6xyovandu7pjhogoaqp
280
+ - test/dummy/storage/9w/a4/9wa4c20p4dvm1cd5thnv9f7ei13w
281
+ - test/dummy/storage/at/kg/atkgs5gwz2xdv9lvqftsg6p7gcpu
282
+ - test/dummy/storage/at/qo/atqomgf3rpb2f6e1tq1yn2xqzojv
283
+ - test/dummy/storage/ba/lq/balqtije6kf82ht4lr70ajaae9kc
284
+ - test/dummy/storage/bf/i1/bfi1ij9rygr6lkx1r0lhgi8o5smx
285
+ - test/dummy/storage/bg/ye/bgyenotrv3aj6lk88edwv0c41pfj
286
+ - test/dummy/storage/bu/xe/buxed4b1l78kcax53fa37awm9ywk
287
+ - test/dummy/storage/d2/c1/d2c11nhikb474oq3q7so0xbhukvj
288
+ - test/dummy/storage/development.sqlite3
289
+ - test/dummy/storage/dk/hy/dkhybxn2o27a8xgvfhsxpgqxa1zf
290
+ - test/dummy/storage/e7/2n/e72nz5cz3wf6qvh4dw4qfnw6ucog
291
+ - test/dummy/storage/eo/4q/eo4qn68m7al0ehhe0s23ycuzkjto
292
+ - test/dummy/storage/ew/8s/ew8sejdsx8ddmrzkvfa37ebz1ts1
293
+ - test/dummy/storage/f8/q1/f8q1kpg2tou8ru0afj8d2gy6ym5p
294
+ - test/dummy/storage/fr/55/fr558uhp1k93jzhb4butyi2ry51t
295
+ - test/dummy/storage/g4/nh/g4nhx1zxbeiegqpgn8ppsl1yhm0t
296
+ - test/dummy/storage/gg/r5/ggr51egxhqfh4w5eluzs47qceb76
297
+ - test/dummy/storage/gh/ua/ghuaagralqmjy8rkbwmuv3010lvs
298
+ - test/dummy/storage/gx/uh/gxuhmf52ufli3m7ng8irp8ghxa1v
299
+ - test/dummy/storage/h0/m1/h0m1emy251xus1d9qh6u25dzy18o
300
+ - test/dummy/storage/hh/kc/hhkc2q8paptyvhw2m5hlwylhtfo5
301
+ - test/dummy/storage/hq/0q/hq0q04kr6qzrp0qaee8rehcp2tzx
302
+ - test/dummy/storage/ii/g1/iig1ge3fsjitai4g2fkq4qt369wh
303
+ - test/dummy/storage/io/f0/iof0mv7w8qjd6m826g52pzyxedet
304
+ - test/dummy/storage/jk/2i/jk2ifmx6ac35ubk3esufnm6bn1m1
305
+ - test/dummy/storage/jw/4t/jw4trdeyfkw3j8z70xcnr9a7gqe5
306
+ - test/dummy/storage/ke/k2/kek24leksglm1rs2a78mfmot0p3s
307
+ - test/dummy/storage/kh/6d/kh6doaxxwxiyes0yqz2dmmpajkzv
308
+ - test/dummy/storage/kj/7n/kj7nookjhisagd80z8hlv3wn50am
309
+ - test/dummy/storage/kq/lf/kqlf5udtrgrk4v55qodxyt6i68p8
310
+ - test/dummy/storage/ky/33/ky334jbo8eb08pj9qbe919iz91mh
311
+ - test/dummy/storage/lt/zw/ltzw4lur2bheit1273ogpfzhv7j1
312
+ - test/dummy/storage/m2/ve/m2vejmyttn1ium81dopppom6vum6
313
+ - test/dummy/storage/m8/d4/m8d4r9iauedq8wlpvnx1f3ou0jwg
314
+ - test/dummy/storage/m9/ee/m9eetioklzatyff94gq0vn1cga1n
315
+ - test/dummy/storage/ma/v0/mav084zvmyoh1a8i7dcwqy2aaoi9
316
+ - test/dummy/storage/mg/pa/mgpauiu02i28j3poef65k3q0gfpw
317
+ - test/dummy/storage/mm/8g/mm8gp5evncb1ol1lj2jlmra2ixij
318
+ - test/dummy/storage/mm/d2/mmd21x8c1amgnidzw0wowiwug4g3
319
+ - test/dummy/storage/n2/qr/n2qro0y9heko9cwxlf10wiqiipsw
320
+ - test/dummy/storage/n8/b7/n8b7b7qgu6jtw577dnn10jrrmszs
321
+ - test/dummy/storage/n8/p2/n8p2ine0qqhphn09kqtxco4y7g0a
322
+ - test/dummy/storage/nk/vh/nkvhgk7snpdy2ak6k02htxx9swp7
323
+ - test/dummy/storage/nn/s0/nns0nggo0x645ytco52adnsi4myp
324
+ - test/dummy/storage/nu/kz/nukzl7cckkzh68i7kyjkm9mzw7c0
325
+ - test/dummy/storage/nv/8v/nv8vyoghcde1yr1bjpsw4327qt7s
326
+ - test/dummy/storage/of/on/ofonhf1gs26k3dpj6o7b0ktzfowh
327
+ - test/dummy/storage/pl/pf/plpfs59hvdoogj9gdweqta36csqn
328
+ - test/dummy/storage/q5/g5/q5g55ekmscu10pzfw6q4syigt81g
329
+ - test/dummy/storage/q5/kc/q5kcr9twyb9v4mh31pay0t7nkuwu
330
+ - test/dummy/storage/qa/xd/qaxdngi74r52ahqg1pz8hjddeajc
331
+ - test/dummy/storage/r7/5v/r75vadn34ak53vinylgnfdl1s8rt
332
+ - test/dummy/storage/rj/rg/rjrghnyzyvxpkjw1a57mrloz72x1
333
+ - test/dummy/storage/se/h7/seh7eorfoanpp6de62pubv7kyu1a
334
+ - test/dummy/storage/sj/i1/sji1oj12soz2fcjcoz0gejvzo8to
335
+ - test/dummy/storage/sn/2r/sn2rku9thay4hbcbt926an69maku
336
+ - test/dummy/storage/sw/jm/swjmbmxou3tnarcirxc6gdycxh91
337
+ - test/dummy/storage/sz/mq/szmqlydvpgqaw7p3v0wh444wtcif
229
338
  - test/dummy/storage/test.sqlite3
230
- - test/dummy/storage/x6/pl/x6plznfuhrsyjn9pox2a6xgmcs3x
231
- - test/dummy/storage/yq/sv/yqsvw5a72b3fv719zq8a6yb7lv0j
339
+ - test/dummy/storage/tg/by/tgbyrdvg94ivhhy2z59e8l9fod10
340
+ - test/dummy/storage/u5/vm/u5vmz08tuayqggd436et8fiaeml1
341
+ - test/dummy/storage/u6/pf/u6pf4yky0vbmvid3fa3lm4lre68g
342
+ - test/dummy/storage/ub/ql/ubqlmlilt8ujgdpngcm1zae41kgy
343
+ - test/dummy/storage/un/29/un29e9khqism72ag27ojccmn5sds
344
+ - test/dummy/storage/ux/ns/uxnsvuk4rr1p67n1oq6tmraz0gaw
345
+ - test/dummy/storage/v1/qo/v1qor0zxg3lctk9mbwyos3oag9gj
346
+ - test/dummy/storage/v8/ok/v8okmd7374w1obna13a7anllx2vu
347
+ - test/dummy/storage/vd/tf/vdtfmz2ctis3dr1r35do9bow2xj5
348
+ - test/dummy/storage/vo/dg/vodgq1inccnujjt3auber7tt8w8o
349
+ - test/dummy/storage/vp/oe/vpoeiq00tf9pk0jcjlccomkju1zc
350
+ - test/dummy/storage/vu/kg/vukgoj6qf96bhealui2yaeyn4n72
351
+ - test/dummy/storage/w7/2z/w72zoqu7v6v6jp0tpy671dcbvpow
352
+ - test/dummy/storage/wa/3f/wa3fncsnozc6n4xfu32gw34geqcd
353
+ - test/dummy/storage/wy/ix/wyixbqx3f6a4agb8bjhrtpblpaua
354
+ - test/dummy/storage/xd/st/xdsttma3tqt7mex0vhp1vsm3dq16
355
+ - test/dummy/storage/xv/ej/xvejm2e064bnpunx3nmktaqs0x90
356
+ - test/dummy/storage/xx/py/xxpyyodssq2xmp57qrtvuw0wchwk
357
+ - test/dummy/storage/xz/ik/xzikejc5sohi3zexa93s9xmg4jst
358
+ - test/dummy/storage/y4/g8/y4g8teo86blcv0zysa2d2jawvk6i
359
+ - test/dummy/storage/y9/58/y958xli6aoktx1ehuyjc1k8dcbzv
360
+ - test/dummy/storage/yj/lw/yjlw8bf70iujb16deja8ae43rqbc
361
+ - test/dummy/storage/z3/qy/z3qyb9avbucwhxa8909rpfued0y5
362
+ - test/dummy/storage/zr/wu/zrwudcg4kgo7r0jemszuzok8grqp
363
+ - test/dummy/tmp/local_secret.txt
364
+ - test/integration/encrypted_blob_proxy_controller_test.rb
232
365
  - test/integration/encrypted_blobs_controller_test.rb
233
366
  - test/lib/encrypted_disk_service_test.rb
234
367
  - test/lib/encrypted_mirror_service_test.rb