crypt_checkpass 1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,214 @@
1
+ #! /your/favourite/path/to/ruby
2
+ # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*-
3
+ # -*- frozen_string_literal: true -*-
4
+ # -*- warn_indent: true -*-
5
+
6
+ # Copyright (c) 2018 Urabe, Shyouhei
7
+ #
8
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ # of this software and associated documentation files (the "Software"), to deal
10
+ # in the Software without restriction, including without limitation the rights
11
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ # copies of the Software, and to permit persons to whom the Software is
13
+ # furnished to do so, subject to the following conditions:
14
+ #
15
+ # The above copyright notice and this permission notice shall be
16
+ # included in all copies or substantial portions of the Software.
17
+ #
18
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ # SOFTWARE.
25
+
26
+ require 'securerandom'
27
+ require_relative 'phc_string_format'
28
+
29
+ # This class is to support RFC7914-related hash variants.
30
+ #
31
+ # ### Format:
32
+ #
33
+ # Life gets extremely hard here because Ruby's `scrypt` gem does not follow the
34
+ # Modular Crypt Format. You cannot tell if a string is scrypt-generated or not
35
+ # by looking at its beginning.
36
+ #
37
+ # ```ruby
38
+ # %r{
39
+ # (?<N> [0-9a-f]+ ){0}
40
+ # (?<r> [0-9a-f]+ ){0}
41
+ # (?<p> [0-9a-f]+ ){0}
42
+ # (?<salt> [0-9a-f]+ ){0}
43
+ # (?<csum> [0-9a-f]+ ){0}
44
+ #
45
+ # \A \g<N>
46
+ # [$] \g<r>
47
+ # [$] \g<p>
48
+ # [$] \g<salt>
49
+ # [$] \g<csum>
50
+ # \z
51
+ # }x
52
+ # ```
53
+ #
54
+ # - `N` is the CPU/Memory cost parameter N ("costParameter").
55
+ #
56
+ # - `r` is the block size parameter r ("blockSize").
57
+ #
58
+ # - `p` is the parallelization parameter p ("parallelizationParameter").
59
+ #
60
+ # - `salt` is the salt string.
61
+ #
62
+ # - `csum` is the checksum strgng.
63
+ #
64
+ # All of above fields are represented as hexadecimal numbers.
65
+ #
66
+ # This is too different from other password hashs. To ease the situation we
67
+ # also follow extra format that is compatible with Python's Passlib and npm's
68
+ # @phc/scrypt generates.
69
+ #
70
+ # ```ruby
71
+ # %r{
72
+ # (?<id> scrypt ){0}
73
+ # (?<ln> ln=[1-9][0-9]* ){0}
74
+ # (?<r> r=[1-9][0-9]* ){0}
75
+ # (?<p> p=[1-9][0-9]* ){0}
76
+ # (?<salt> [a-zA-Z0-9+/]* ){0}
77
+ # (?<csum> [a-zA-Z0-9+/]* ){0}
78
+ #
79
+ # \A [$] \g<id>
80
+ # [$] \g<ln>
81
+ # [,] \g<r>
82
+ # [,] \g<p>
83
+ # [$] \g<salt>
84
+ # [$] \g<csum>
85
+ # \z
86
+ # }x
87
+ # ```
88
+ #
89
+ # - This is a strict PHC string format. See also
90
+ # {CryptCheckpass::PHCStringFormat}
91
+ #
92
+ # - Parameters are `ln`, `r`, and `p` where `ln` deontes log2(N).
93
+ #
94
+ # ### Other formats:
95
+ #
96
+ # Seems there are no such thing like a standard way to encode scrypt-generated
97
+ # passwords. Lots of wild formats are seen. We could support them if they
98
+ # have actual usage and to be migrated to another format.
99
+ #
100
+ # - variant `$7$`: seem in http://mail.tarsnap.com/scrypt/msg00063.html. Not
101
+ # sure if it has actual applications.
102
+ #
103
+ # - variant `$s1$`: https://github.com/technion/libscrypt generates this.
104
+ #
105
+ # - variant `$$scrypt-mcf$`: the default output of https://libpasta.github.io
106
+ #
107
+ # - Recent OpenSSL (1.1.0+) does have EVP_PBE_scrypt() implemented, but
108
+ # generates pure-binary raw checksums without any formats.
109
+ #
110
+ # @see https://en.wikipedia.org/wiki/Scrypt
111
+ # @see https://tools.ietf.org/html/rfc7914
112
+ # @see https://blog.ircmaxell.com/2014/03/why-i-dont-recommend-scrypt.html
113
+ # @example
114
+ # crypt_newhash 'password', id: 'scrypt'
115
+ # # => "$scrypt$ln=8,r=8,p=1$aL2uvFKrfoVkxAgy1j/Y4OAJ8D0p1yP/uqFg3UU8t64$/xZGQyALLQrKzaBRGwzGCw+FGgRqFwyCfZddC5qvZYA"
116
+ # @example
117
+ # crypt_checkpass? 'password', '$scrypt$ln=8,r=8,p=1$aL2uvFKrfoVkxAgy1j/Y4OAJ8D0p1yP/uqFg3UU8t64$/xZGQyALLQrKzaBRGwzGCw+FGgRqFwyCfZddC5qvZYA'
118
+ # # => true
119
+
120
+ class CryptCheckpass::Scrypt < CryptCheckpass
121
+
122
+ # (see CryptCheckpass.understand?)
123
+ def self.understand? str
124
+ return match? str, understander
125
+ end
126
+
127
+ # (see CryptCheckpass.checkpass?)
128
+ def self.checkpass? pass, hash
129
+ require 'scrypt'
130
+
131
+ case hash
132
+ when /\A\$scrypt\$/ then return checkpass_phc pass, hash
133
+ else return checkpass_gem pass, hash
134
+ end
135
+ end
136
+
137
+ # (see CryptCheckpass.provide?)
138
+ def self.provide? id
139
+ return id == 'scrypt'
140
+ end
141
+
142
+ # (see CryptCheckpass.newhash)
143
+ #
144
+ # @param pass [String] raw binary password string.
145
+ # @param id [String] name of the algorithm (ignored)
146
+ # @param ln [Integer] cost parameter in log2.
147
+ # @param r [Integer] block size.
148
+ # @param p [Integer] parallelism parameter.
149
+ def self.newhash pass, id: 'scrypt', ln: 8, r: 8, p: 1
150
+ require 'scrypt'
151
+
152
+ salt = SecureRandom.random_bytes ::SCrypt::Engine::DEFAULTS[:salt_size]
153
+ klen = ::SCrypt::Engine::DEFAULTS[:key_len]
154
+ csum = ::SCrypt::Engine.scrypt pass, salt, 2 ** ln, r, p, klen
155
+ return phcencode 'scrypt', { ln: ln, r: r, p: p }, salt, csum
156
+ end
157
+ end
158
+
159
+ # helper routines
160
+ class << CryptCheckpass::Scrypt
161
+ include CryptCheckpass::PHCStringFormat
162
+
163
+ private
164
+
165
+ def checkpass_phc pass, hash
166
+ require 'consttime_memequal'
167
+
168
+ json = phcdecode hash
169
+ ln, r, p = json[:params].values_at :ln, :r, :p
170
+ expected = json[:csum]
171
+ salt = json[:salt]
172
+ klen = ::SCrypt::Engine::DEFAULTS[:key_len]
173
+ actual = ::SCrypt::Engine.scrypt pass, salt, 2 ** ln, r, p, klen
174
+ return consttime_memequal? actual, expected
175
+ end
176
+
177
+ def checkpass_gem pass, hash
178
+ obj = ::SCrypt::Password.new hash
179
+ return obj == pass
180
+ end
181
+
182
+ def understander
183
+ return %r{
184
+ (?<n1> [0-9a-f]+ ){0}
185
+ (?<r1> [0-9a-f]+ ){0}
186
+ (?<p1> [0-9a-f]+ ){0}
187
+ (?<salt1> [0-9a-f]+ ){0}
188
+ (?<csum1> [0-9a-f]+ ){0}
189
+
190
+ (?<id2> scrypt ){0}
191
+ (?<ln2> ln=[1-9][0-9]* ){0}
192
+ (?<r2> r=[1-9][0-9]* ){0}
193
+ (?<p2> p=[1-9][0-9]* ){0}
194
+ (?<salt2> [a-zA-Z0-9+/]* ){0}
195
+ (?<csum2> [a-zA-Z0-9+/]* ){0}
196
+
197
+ \A (?:
198
+ \g<n1>
199
+ [$] \g<r1>
200
+ [$] \g<p1>
201
+ [$] \g<salt1>
202
+ [$] \g<csum1>
203
+
204
+ |
205
+ [$] \g<id2>
206
+ [$] \g<ln2>
207
+ [,] \g<r2>
208
+ [,] \g<p2>
209
+ [$] \g<salt2>
210
+ [$] \g<csum2>
211
+ ) \z
212
+ }x
213
+ end
214
+ end
@@ -0,0 +1,162 @@
1
+ #! /your/favourite/path/to/ruby
2
+ # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*-
3
+ # -*- frozen_string_literal: true -*-
4
+ # -*- warn_indent: true -*-
5
+
6
+ # Copyright (c) 2018 Urabe, Shyouhei
7
+ #
8
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ # of this software and associated documentation files (the "Software"), to deal
10
+ # in the Software without restriction, including without limitation the rights
11
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ # copies of the Software, and to permit persons to whom the Software is
13
+ # furnished to do so, subject to the following conditions:
14
+ #
15
+ # The above copyright notice and this permission notice shall be
16
+ # included in all copies or substantial portions of the Software.
17
+ #
18
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ # SOFTWARE.
25
+
26
+ # The sha256cyrpt / sha512crypt by Ulrich Drepper. Default for `/etc/shadow`
27
+ # of most Linux distributions. Also no security flaws are known at the moment.
28
+ #
29
+ # ### Newhash:
30
+ #
31
+ # You can use `crypto_newhash` to create a new password hash using SHA2:
32
+ #
33
+ # ```ruby
34
+ # crypt_newhash(password, id: 'sha256', rounds: 1024)
35
+ # ```
36
+ #
37
+ # where:
38
+ #
39
+ # - `password` is the raw binary password that you want to digest.
40
+ #
41
+ # - `id` is either "sha256" or "sha512".
42
+ #
43
+ # - `rounds` is for iteration rounds.
44
+ #
45
+ # The generated password hash has following format.
46
+ #
47
+ # ### Format:
48
+ #
49
+ # Hash strings generated by sha256cyrpt is construted like this:
50
+ #
51
+ # ```ruby
52
+ # %r{
53
+ # (?<id> 5 ){0}
54
+ # (?<rounds> rounds=[1-9]\d{3,8} ){0}
55
+ # (?<salt> [A-Za-z0-9./]{,16} ){0}
56
+ # (?<csum> [A-Za-z0-9./]{43} ){0}
57
+ #
58
+ # \A [$] \g<id>
59
+ # (?: [$] \g<rounds> )?
60
+ # [$] \g<salt>
61
+ # [$] \g<csum>
62
+ # \z
63
+ # }x
64
+ # ```
65
+ #
66
+ # That of sha512crypt is construted like this:
67
+ #
68
+ # ```ruby
69
+ # %r{
70
+ # (?<id> 6 ){0}
71
+ # (?<rounds> rounds=[1-9]\d{3,8} ){0}
72
+ # (?<salt> [A-Za-z0-9./]{,16} ){0}
73
+ # (?<csum> [A-Za-z0-9./]{86} ){0}
74
+ #
75
+ # \A [$] \g<id>
76
+ # (?: [$] \g<rounds> )?
77
+ # [$] \g<salt>
78
+ # [$] \g<csum>
79
+ # \z
80
+ # }x
81
+ # ```
82
+ #
83
+ # - `id` is 5 for sha256cyrpt, 6 for sha512crypt.
84
+ #
85
+ # - `rounds` is, if present, the stretch rounds in decimal integer. Should be
86
+ # in range of 1,000 to 999,999,999 inclusive.
87
+ #
88
+ # - `salt` and `csum` are the salt and checksum strings. Both are encoded in
89
+ # base64-like strings that do not strictly follow RFC4648. The only
90
+ # difference between `$5$` and `$6$` is the length of csum.
91
+ #
92
+ # @see https://www.akkadia.org/drepper/SHA-crypt.txt
93
+ # @example
94
+ # crypt_newhash 'password', id: 'sha256'
95
+ # # => "$5$eWGIDuRO1LEg8sAB$Pjdxj3AVy4GnFfeOfz8Ek1Gn.vDwTFMMyNk56x/lc.4"
96
+ # @example
97
+ # crypt_checkpass? 'password', '$5$eWGIDuRO1LEg8sAB$Pjdxj3AVy4GnFfeOfz8Ek1Gn.vDwTFMMyNk56x/lc.4'
98
+ # # => true
99
+ # @example
100
+ # crypt_newhash 'password', id: 'sha512'
101
+ # # => "$6$oIlkXbDGlU.HktGx$L7xkRSQYLe/yCbz6hIM2JSY6EMtkr/CyvR71Bhr9VkotfEOUiwY8A0rAuSFmO1titWLA8hTKQXWl3ZX0QqokS0"
102
+ # @example
103
+ # crypt_checkpass? 'password', '$6$oIlkXbDGlU.HktGx$L7xkRSQYLe/yCbz6hIM2JSY6EMtkr/CyvR71Bhr9VkotfEOUiwY8A0rAuSFmO1titWLA8hTKQXWl3ZX0QqokS0'
104
+ # # => true
105
+ class CryptCheckpass::SHA2 < CryptCheckpass
106
+
107
+ # (see CryptCheckpass.understand?)
108
+ def self.understand? str
109
+ md = %r{
110
+ (?<id> 5 | 6 ){0}
111
+ (?<rounds> rounds=[1-9]\d{3,8} ){0}
112
+ (?<salt> [A-Za-z0-9./]{,16} ){0}
113
+ (?<csum> [A-Za-z0-9./]{43,86} ){0}
114
+
115
+ \A [$] \g<id>
116
+ (?: [$] \g<rounds> )?
117
+ [$] \g<salt>
118
+ [$] \g<csum>
119
+ \z
120
+ }x.match str
121
+ return false unless md
122
+ case md['id']
123
+ when '5' then return md['csum'].length == 43
124
+ when '6' then return md['csum'].length == 86
125
+ end
126
+ end
127
+
128
+ # (see CryptCheckpass.checkpass?)
129
+ def self.checkpass? pass, hash
130
+ require 'unix-crypt', 'unix_crypt'
131
+
132
+ return UnixCrypt.valid? pass, hash
133
+ end
134
+
135
+ # (see CryptCheckpass.provide?)
136
+ def self.provide? id
137
+ case id when 'sha256', 'sha512' then
138
+ return true
139
+ else
140
+ return false
141
+ end
142
+ end
143
+
144
+ # (see CryptCheckpass.newhash)
145
+ #
146
+ # @param pass [String] raw binary password string.
147
+ # @param id [String] name of the algorithm.
148
+ # @param rounds [Integer] rounds of stretching.
149
+ def self.newhash pass, id: 'sha256', rounds: nil
150
+ require 'unix-crypt', 'unix_crypt'
151
+
152
+ case id
153
+ when 'sha256' then
154
+ klass = UnixCrypt::SHA256
155
+ when 'sha512' then
156
+ klass = UnixCrypt::SHA512
157
+ else
158
+ raise ArgumentError, 'unknown id: %p', id
159
+ end
160
+ return klass.build pass, nil, rounds
161
+ end
162
+ end
metadata ADDED
@@ -0,0 +1,201 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: crypt_checkpass
3
+ version: !ruby/object:Gem::Version
4
+ version: '1'
5
+ platform: ruby
6
+ authors:
7
+ - Urabe, Shyouhei
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-06-25 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: pry-byebug
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rdoc
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: redcarpet
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 0.49.0
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 0.49.0
97
+ - !ruby/object:Gem::Dependency
98
+ name: simplecov
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: test-unit
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '3'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '3'
125
+ - !ruby/object:Gem::Dependency
126
+ name: yard
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: consttime_memequal
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :runtime
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ description: Check password hash, like OpenBSD's crypt_checkpass(3) / PHP's password_verify()
154
+ email: shyouhei@ruby-lang.org
155
+ executables: []
156
+ extensions: []
157
+ extra_rdoc_files: []
158
+ files:
159
+ - ".gitignore"
160
+ - ".rubocop.yml"
161
+ - ".yardopts"
162
+ - Gemfile
163
+ - LICENSE.txt
164
+ - README.md
165
+ - Rakefile
166
+ - bin/console
167
+ - bin/setup
168
+ - crypt_checkpass.gemspec
169
+ - lib/crypt_checkpass.rb
170
+ - lib/crypt_checkpass/api.rb
171
+ - lib/crypt_checkpass/argon2.rb
172
+ - lib/crypt_checkpass/bcrypt.rb
173
+ - lib/crypt_checkpass/pbkdf2.rb
174
+ - lib/crypt_checkpass/phc_string_format.rb
175
+ - lib/crypt_checkpass/scrypt.rb
176
+ - lib/crypt_checkpass/sha2.rb
177
+ homepage: https://github.com/shyouhei/crypt_checkpass
178
+ licenses:
179
+ - MIT
180
+ metadata: {}
181
+ post_install_message:
182
+ rdoc_options: []
183
+ require_paths:
184
+ - lib
185
+ required_ruby_version: !ruby/object:Gem::Requirement
186
+ requirements:
187
+ - - ">="
188
+ - !ruby/object:Gem::Version
189
+ version: 2.0.0
190
+ required_rubygems_version: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - ">="
193
+ - !ruby/object:Gem::Version
194
+ version: '0'
195
+ requirements: []
196
+ rubyforge_project:
197
+ rubygems_version: 2.5.2.2
198
+ signing_key:
199
+ specification_version: 4
200
+ summary: provides crypt_checkpass / crypt_newhash
201
+ test_files: []