├── .gitignore ├── .rspec ├── .travis.yml ├── CONTRIBUTING.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin └── itamae-secrets ├── itamae-secrets.gemspec ├── lib └── itamae │ ├── secrets.rb │ └── secrets │ ├── aes_key.rb │ ├── cli.rb │ ├── decryptor.rb │ ├── encryptor.rb │ ├── keychain.rb │ ├── store.rb │ └── version.rb ├── script ├── console └── setup └── spec ├── itamae ├── secrets │ └── store_spec.rb └── secrets_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.3.0 4 | before_install: gem install bundler -v 1.10.5 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | __Security issues?__ Send me directly at `security@sorah.jp`. My GPG key is available here: ([SSL](https://github.com/sorah/sorah.jp/tree/master/source/pgp-pubkeys)) 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in itamae-secrets.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Shota Fukumori (sora_h) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Itamae::Secrets - Encrypted Data Bag for Itamae 2 | 3 | This is [itamae](https://github.com/itamae-kitchen/itamae) plugin that provides store for secrets, like encrypted data bag in chef. 4 | 5 | ## Installation 6 | 7 | ```ruby 8 | gem 'itamae-secrets' 9 | ``` 10 | 11 | or 12 | 13 | ``` 14 | $ gem install itamae-secrets 15 | ``` 16 | 17 | ## Basic 18 | 19 | - `itamae-secrets` command for storing data or manually reading 20 | - `Itamae::Secrets` interface for itamae recipes 21 | - Data are stored in _base directory._ 22 | - You must avoid `${base}/keys` from checked into VCS. (`.gitignore` it!) 23 | 24 | ## Walkthrough 25 | 26 | ### Generate a key 27 | 28 | ##### randomly 29 | 30 | ``` 31 | $ itamae-secrets newkey --base=./secret --method=aes-random 32 | ``` 33 | 34 | ##### from passphrase 35 | 36 | ``` 37 | $ itamae-secrets newkey --base=./secret --method=aes-passphrase 38 | ``` 39 | 40 | Both generates `./secret/keys/default`. Make sure `./secret/keys` be excluded from VCS. 41 | 42 | ### Store value 43 | 44 | ``` 45 | $ itamae-secrets set --base=./secret awesome_secret value 46 | ``` 47 | 48 | (when omit `value`, it'll read from STDIN until EOF. You can also use `--noecho` if you want hide value in your terminal's buffer completely.) 49 | 50 | ### Reading data from itamae 51 | 52 | on your itamae recipe, do: 53 | 54 | ``` ruby 55 | require 'itamae/secrets' 56 | node[:secrets] = Itamae::Secrets(File.join(__dir__, 'secret')) 57 | 58 | # Use it 59 | p node[:secrets][:awesome_secret] 60 | ``` 61 | 62 | ### Reading data from CLI 63 | 64 | ``` 65 | $ itamae-secrets get --base=./secret awesome_secret 66 | ``` 67 | 68 | ### Remembering `--base` 69 | 70 | ``` 71 | $ echo 'base: ./secret' >> .itamae-secrets.yml 72 | ``` 73 | 74 | ## Development 75 | 76 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 77 | 78 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 79 | 80 | ## Contributing 81 | 82 | Bug reports and pull requests are welcome on GitHub at https://github.com/sorah/itamae-secrets. 83 | 84 | __Security issues?__ Send me directly at `security@sorah.jp`. My GPG key is available here: ([SSL](https://github.com/sorah/sorah.jp/tree/master/source/pgp-pubkeys)) 85 | 86 | 87 | ## License 88 | 89 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). 90 | 91 | ## To-dos 92 | 93 | - [ ] Missing test :( 94 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /bin/itamae-secrets: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'itamae/secrets/cli' 3 | 4 | Itamae::Secrets::Cli.start 5 | -------------------------------------------------------------------------------- /itamae-secrets.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'itamae/secrets/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "itamae-secrets" 8 | spec.version = Itamae::Secrets::VERSION 9 | spec.authors = ["Shota Fukumori (sora_h)"] 10 | spec.email = ["her@sorah.jp"] 11 | 12 | spec.summary = %q{Encrypted Data Bag for itamae} 13 | spec.homepage = "https://github.com/sorah/itamae-secrets" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 17 | spec.bindir = "bin" 18 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_dependency 'thor' 22 | 23 | spec.add_development_dependency "bundler" 24 | spec.add_development_dependency "rake" 25 | spec.add_development_dependency "rspec" 26 | end 27 | -------------------------------------------------------------------------------- /lib/itamae/secrets.rb: -------------------------------------------------------------------------------- 1 | require "itamae/secrets/version" 2 | require "itamae/secrets/store" 3 | 4 | module Itamae 5 | def self.Secrets(*args) 6 | Itamae::Secrets::Store.new *args 7 | end 8 | 9 | module Secrets 10 | end 11 | 12 | end 13 | -------------------------------------------------------------------------------- /lib/itamae/secrets/aes_key.rb: -------------------------------------------------------------------------------- 1 | require 'openssl' 2 | require 'json' 3 | 4 | module Itamae 5 | module Secrets 6 | class AesKey 7 | AES1_KEY_LEN = OpenSSL::Cipher.new('aes-256-gcm').key_len 8 | 9 | def self.key_len_for_type(type) 10 | case type 11 | when 'aes1' 12 | AES1_KEY_LEN 13 | else 14 | raise ArgumentError, "unknown type #{type.inspect}" 15 | end 16 | end 17 | 18 | def self.generate_random(name) 19 | key_len = key_len_for_type('aes1') 20 | new name, 'aes1', OpenSSL::Random.random_bytes(key_len) 21 | end 22 | 23 | def self.generate_pkcs5(name, passphrase) 24 | key_len = key_len_for_type('aes1') 25 | 26 | salt = OpenSSL::Digest::SHA256.digest(name) 27 | key = OpenSSL::PKCS5.pbkdf2_hmac(passphrase, salt, 30000, key_len, OpenSSL::Digest::SHA256.new) 28 | 29 | new name, 'aes1', key 30 | end 31 | 32 | def self.load_json(json) 33 | data = JSON.parse(json) 34 | new(data['name'], data['type'], data['key'].unpack('m*')[0]) 35 | end 36 | 37 | def initialize(name, type, key) 38 | raise ArgumentError, "name must not contain slashes, commas, backslackes" if name.include?("\\") || name.include?(?/) || name.include?(?:) 39 | @name = name 40 | @type = type 41 | @key = key 42 | end 43 | 44 | attr_reader :name, :type, :key 45 | 46 | def algorithm_compatible?(algorithm) 47 | algorithm == 'aes-256-gcm' 48 | end 49 | 50 | def to_s 51 | key 52 | end 53 | 54 | def to_json 55 | { 56 | name: name, 57 | type: type, 58 | key: [key].pack('m*'), 59 | }.to_json 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/itamae/secrets/cli.rb: -------------------------------------------------------------------------------- 1 | require 'thor' 2 | require 'yaml' 3 | require 'pathname' 4 | 5 | require 'itamae/secrets/aes_key' 6 | require 'itamae/secrets/store' 7 | 8 | module Itamae 9 | module Secrets 10 | class Cli < Thor 11 | class_option :base, type: :string, desc: 'path to base directory for storing secrets and keys' 12 | 13 | 14 | desc 'newkey [KEYNAME]', 'generate then save key' 15 | method_option :method, type: :string, required: true, desc: 'generating method (aes-random, aes-passphrase)' 16 | method_option :confirm_passphrase, type: :boolean, default: true, desc: 'Confirm passphrase when asking' 17 | 18 | def newkey(name='default') 19 | if keychain.exist?(name) 20 | raise ArgumentError, "key #{name} already exists" 21 | end 22 | 23 | key = case options[:method] || config['generate_method'] 24 | when 'aes-random' 25 | AesKey.generate_random(name) 26 | when 'aes-passphrase' 27 | passphrase = ask_noecho('Passphrase:', $stdin.tty?) 28 | AesKey.generate_pkcs5(name, passphrase) 29 | else 30 | raise ArgumentError, "Unknown method: #{options[:method] || config['generate_method']}" 31 | end 32 | 33 | keychain.save(key) 34 | end 35 | 36 | desc 'set VARNAME [VALUE]', 'store value (when VALUE is omitted, read from STDIN)' 37 | method_option :key, type: :string, default: 'default', desc: 'key name' 38 | method_option :noecho, type: :boolean, desc: 'Ask one-line value with noecho when stdin is tty' 39 | def set(name, value = nil) 40 | value ||= if options[:noecho] 41 | ask_noecho("#{name}:", false) 42 | else 43 | $stdin.read.chomp 44 | end 45 | 46 | store.store(name, value, options[:key]) 47 | end 48 | 49 | desc 'get VARNAME', 'read value' 50 | def get(name) 51 | value = store[name] 52 | if value 53 | puts value 54 | else 55 | exit 1 56 | end 57 | end 58 | 59 | private 60 | 61 | def store 62 | @store ||= Store.new base_dir 63 | end 64 | 65 | def keychain 66 | store.keychain 67 | end 68 | 69 | def base_dir 70 | unless config['base'] 71 | raise ArgumentError, 'Missing --base' 72 | end 73 | Pathname.new config['base'] 74 | end 75 | 76 | def config 77 | @config ||= config_file.merge( 78 | 'base' => config_file['base'] || options[:base], 79 | ) 80 | end 81 | 82 | def config_file 83 | @config_file ||= if File.exist?('./.itamae-secrets.yml') 84 | YAML.load_file('./.itamae-secrets.yml') 85 | else 86 | {} 87 | end 88 | end 89 | 90 | def ask_noecho(prompt, confirm = true) 91 | io_console = false 92 | begin 93 | require 'io/console' 94 | io_console = true 95 | rescue LoadError 96 | end 97 | 98 | get = -> do 99 | if $stdin.tty? 100 | $stdin.noecho { $stdin.gets.chomp } 101 | else 102 | $stdin.gets.chomp 103 | end 104 | end 105 | 106 | loop do 107 | $stdout.print "#{prompt} " 108 | value = get.call 109 | 110 | break value unless confirm 111 | 112 | $stdout.print "(confirm) #{prompt} " 113 | break value if value == get.call 114 | 115 | $stderr.puts "Confirmation didn't match..." 116 | end 117 | end 118 | end 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /lib/itamae/secrets/decryptor.rb: -------------------------------------------------------------------------------- 1 | require 'openssl' 2 | 3 | module Itamae 4 | module Secrets 5 | class Decryptor 6 | ALGORITHM = 'aes-256-gcm' 7 | 8 | def self.load_json(json, key = nil) 9 | data = JSON.parse(json) 10 | 11 | raise ArgumentError, "unknown version #{data['version'].inspect}" if data['version'] != 1 12 | raise ArgumentError, "unknown version #{data['algorithm'].inspect}" if data['algorithm'] != ALGORITHM 13 | 14 | new( 15 | data['ciphertext'], 16 | data['auth_tag'], 17 | data['iv'], 18 | data['key_name'], 19 | key 20 | ) 21 | end 22 | 23 | def initialize(ciphertext, auth_tag, iv, key_name, key = nil) 24 | ensure_algorithm_key_compatiblity!(key) if key 25 | @ciphertext = ciphertext 26 | @auth_tag = auth_tag 27 | @iv = iv 28 | @key_name = key_name 29 | @key = key 30 | end 31 | 32 | attr_reader :ciphertext, :auth_tag, :iv, :key_name 33 | attr_accessor :key 34 | 35 | def key=(other) 36 | raise "can't overwrite" if @key 37 | ensure_algorithm_key_compatiblity!(other) 38 | @key = other 39 | end 40 | 41 | def plaintext 42 | @plaintext ||= begin 43 | txt = cipher.update(ciphertext.unpack('m*')[0]) 44 | txt << cipher.final 45 | end 46 | end 47 | 48 | def version 49 | 1 50 | end 51 | 52 | def algorithm 53 | ALGORITHM 54 | end 55 | 56 | def cipher 57 | @cipher ||= OpenSSL::Cipher.new(algorithm).tap do |c| 58 | raise 'key is required to proceed' unless key 59 | c.decrypt 60 | c.key = key.to_s 61 | c.iv = iv.unpack('m*')[0] 62 | c.auth_data = '' 63 | c.auth_tag = auth_tag.unpack('m*')[0] 64 | end 65 | end 66 | 67 | private 68 | 69 | def ensure_algorithm_key_compatiblity!(key) 70 | unless key.algorithm_compatible?(algorithm) 71 | raise ArgumentError, "#{key.type} is not compatible" 72 | end 73 | end 74 | end 75 | end 76 | end 77 | 78 | -------------------------------------------------------------------------------- /lib/itamae/secrets/encryptor.rb: -------------------------------------------------------------------------------- 1 | require 'openssl' 2 | 3 | module Itamae 4 | module Secrets 5 | class Encryptor 6 | ALGORITHM = 'aes-256-gcm' 7 | 8 | def initialize(plaintext, key = nil, iv = nil) 9 | ensure_algorithm_key_compatiblity!(key) if key 10 | @key = key 11 | @iv = iv 12 | @plaintext = plaintext 13 | end 14 | 15 | attr_reader :key, :plaintext 16 | 17 | def key=(other) 18 | raise "can't overwrite" if @key 19 | ensure_algorithm_key_compatiblity!(other) 20 | @key = other 21 | end 22 | 23 | def to_s 24 | { 25 | version: version, 26 | algorithm: algorithm, 27 | key_name: key.name, 28 | ciphertext: ciphertext, 29 | iv: iv, 30 | auth_tag: auth_tag, 31 | }.to_json 32 | end 33 | 34 | alias data to_s 35 | 36 | def ciphertext 37 | @ciphertext ||= begin 38 | data = cipher.update(plaintext) 39 | data << cipher.final 40 | @auth_tag = cipher.auth_tag 41 | [data].pack('m*') 42 | end 43 | end 44 | 45 | def iv 46 | @iv && [@iv].pack('m*') 47 | end 48 | 49 | def auth_tag 50 | if @auth_tag 51 | [@auth_tag].pack('m*') 52 | else 53 | raise '[BUG] auth_tag not exists' 54 | end 55 | end 56 | 57 | def version 58 | 1 59 | end 60 | 61 | def algorithm 62 | ALGORITHM 63 | end 64 | 65 | def cipher 66 | @cipher ||= OpenSSL::Cipher.new(algorithm).tap do |c| 67 | raise 'key is required to proceed' unless key 68 | c.encrypt 69 | c.key = key.to_s 70 | # XXX: avoid generate IV here, but consider if extract to a method like #iv, it have to know Cipher#iv_len... 71 | @iv ||= c.random_iv 72 | c.iv = @iv 73 | c.auth_data = '' 74 | end 75 | end 76 | 77 | private 78 | 79 | def ensure_algorithm_key_compatiblity!(key) 80 | unless key.algorithm_compatible?(algorithm) 81 | raise ArgumentError, "#{key.type} is not compatible" 82 | end 83 | end 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/itamae/secrets/keychain.rb: -------------------------------------------------------------------------------- 1 | require 'pathname' 2 | require 'itamae/secrets/aes_key' 3 | 4 | module Itamae 5 | module Secrets 6 | class Keychain 7 | class KeyNotFound < StandardError; end 8 | 9 | def initialize(path) 10 | @path = Pathname.new(path) 11 | end 12 | 13 | attr_reader :path 14 | 15 | def exist?(name) 16 | @path.join(name).exist? 17 | end 18 | 19 | def load(name) 20 | AesKey.load_json @path.join(name).read 21 | rescue Errno::ENOENT 22 | raise KeyNotFound, "Couldn't find key #{name.inspect}" 23 | end 24 | 25 | def save(key) 26 | open(@path.join(key.name), 'w', 0600) do |io| 27 | io.puts key.to_json 28 | end 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/itamae/secrets/store.rb: -------------------------------------------------------------------------------- 1 | require 'pathname' 2 | 3 | require 'itamae/secrets/keychain' 4 | require 'itamae/secrets/encryptor' 5 | require 'itamae/secrets/decryptor' 6 | 7 | module Itamae 8 | module Secrets 9 | class Store 10 | def initialize(base_dir) 11 | @base_dir = Pathname.new(base_dir) 12 | ensure_base_dir! 13 | end 14 | 15 | attr_reader :base_dir 16 | 17 | def keychain_path 18 | base_dir.join('keys') 19 | end 20 | 21 | def values_path 22 | base_dir.join('values') 23 | end 24 | 25 | def keychain 26 | @keychain ||= Keychain.new(keychain_path) 27 | end 28 | 29 | def [](name) 30 | fetch(name, nil) 31 | end 32 | 33 | def fetch(*args) 34 | if args.size > 2 35 | raise ArgumentError, "wrong number of arguments (#{args.size} for 1..2)" 36 | end 37 | 38 | name = args[0].to_s 39 | validate_name!(name) 40 | 41 | value_path = values_path.join(name) 42 | 43 | if value_path.exist? 44 | encrypted_data = Decryptor.load_json(value_path.read) 45 | encrypted_data.key = keychain.load(encrypted_data.key_name) 46 | JSON.parse(encrypted_data.plaintext)['value'] 47 | else 48 | if args.size == 1 49 | raise KeyError, "key not found: #{name}" 50 | else 51 | args[1] 52 | end 53 | end 54 | end 55 | 56 | def []=(*args) 57 | case args.size 58 | when 2 59 | store(*args) 60 | when 3 61 | store(args[0], args[2], args[1]) 62 | else 63 | raise ArgumentError, "wrong number of arguments (#{args.size} for 2..3)" 64 | end 65 | end 66 | 67 | def store(name, value, key = 'default') 68 | name = name.to_s 69 | validate_name!(name) 70 | value_path = values_path.join(name) 71 | 72 | encrypted_data = Encryptor.new({value: value}.to_json, keychain.load(key)) 73 | 74 | open(value_path, 'w', 0600) do |io| 75 | io.puts encrypted_data.to_s 76 | end 77 | end 78 | 79 | private 80 | 81 | def ensure_base_dir! 82 | unless base_dir.exist? 83 | Dir.mkdir(base_dir) 84 | end 85 | %w(keys values).each do |x| 86 | path = base_dir.join(x) 87 | Dir.mkdir(path) unless File.exist?(path) 88 | end 89 | end 90 | 91 | def validate_name!(name) 92 | # XXX: dupe 93 | raise ArgumentError, "name must not contain slashes, colons, backslackes" if name.include?("\\") || name.include?(?/) || name.include?(?:) 94 | end 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /lib/itamae/secrets/version.rb: -------------------------------------------------------------------------------- 1 | module Itamae 2 | module Secrets 3 | VERSION = "0.2.3" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /script/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "itamae/secrets" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start 15 | -------------------------------------------------------------------------------- /script/setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | bundle install 6 | 7 | # Do any other automated setup that you need to do here 8 | -------------------------------------------------------------------------------- /spec/itamae/secrets/store_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | RSpec.describe Itamae::Secrets::Store do 4 | subject(:store) { described_class.new(base_path) } 5 | 6 | let(:base_path) { RSPEC_TEMP_PATH } 7 | 8 | describe '.fetch' do 9 | subject(:store_fetch) { store.fetch('foo') } 10 | 11 | context 'when arguments excced 2' do 12 | subject(:store_fetch) { store.fetch('foo', 'bar', 'baz') } 13 | 14 | it { expect { store_fetch }.to raise_error(ArgumentError) } 15 | end 16 | 17 | context 'when key is not found' do 18 | it 'returns key error' do 19 | expect { store_fetch }.to raise_error(KeyError, /key not found: foo/) 20 | end 21 | end 22 | 23 | context 'when key is not valid' do 24 | subject(:store_fetch) { store.fetch(invalid_key) } 25 | 26 | let(:invalid_key) { %w(foo:bar foo\\bar foo/bar).sample } 27 | 28 | it 'returns argument error for names having slashes, colons or backslashes' do 29 | expect { store_fetch } 30 | .to raise_error(ArgumentError, /name must not contain slashes, colons, backslackes/) 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/itamae/secrets_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Itamae::Secrets do 4 | it 'has a version number' do 5 | expect(Itamae::Secrets::VERSION).not_to be nil 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | 3 | require 'tmpdir' 4 | RSPEC_TEMP_PATH = Dir.mktmpdir 5 | 6 | require 'itamae/secrets' 7 | --------------------------------------------------------------------------------