├── .gitignore ├── .rspec ├── .travis.yml ├── Gemfile ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── exe └── reversible_cryptography ├── lib ├── reversible_cryptography.rb └── reversible_cryptography │ ├── cli.rb │ ├── message.rb │ └── version.rb ├── reversible_cryptography.gemspec └── spec ├── reversible_cryptography_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.4.1 4 | script: bundle exec rspec 5 | branches: 6 | only: 7 | - master 8 | notifications: 9 | email: false 10 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in reversible_cryptography.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ReversibleCryptography 2 | 3 | Easy-to-use reversible encryption solutions. 4 | 5 | ## Installation 6 | 7 | Add this line to your application's Gemfile: 8 | 9 | ```ruby 10 | gem 'reversible_cryptography' 11 | ``` 12 | 13 | And then execute: 14 | 15 | $ bundle 16 | 17 | Or install it yourself as: 18 | 19 | $ gem install reversible_cryptography 20 | 21 | ## Usage 22 | 23 | ```ruby 24 | encrypted_message = ReversibleCryptography::Message.encrypt("target_message", "password") 25 | # => "md5:388eeae24576572f946e9043a2118b2d:salt:161-225-182-109-143-90-1-28:aes-256-cfb:DHY6DF3+iFzH36FMbeI=" 26 | ReversibleCryptography::Message.decrypt(encrypted_message, "password") == "target_message" 27 | # => true 28 | ``` 29 | 30 | ### CLI 31 | Add `reversible_cryptography` command 32 | 33 | ```shell 34 | $ reversible_cryptography 35 | 36 | Commands: 37 | reversible_cryptography decrypt [TEXT] # Decrypt text or file 38 | reversible_cryptography edit FILE # Edit encrypted file 39 | reversible_cryptography encrypt [TEXT] # Encrypt text or file 40 | reversible_cryptography help [COMMAND] # Describe available commands or one specific command 41 | ``` 42 | 43 | #### Encrypt sample 44 | 45 | ```shell 46 | $ reversible_cryptography encrypt message 47 | Input password: 48 | md5:78e731027d8fd50ed642340b7c9a63b3:salt:252-235-72-88-180-7-195-229:aes-256-cfb:VH2JxqUU9Q== 49 | ``` 50 | 51 | ```shell 52 | cat original.txt 53 | this is secret! 54 | 55 | reversible_cryptography encrypt --password=pass --src-file=original.txt --dst-file=encrypted.txt 56 | 57 | cat encrypted.txt 58 | md5:f5b013aca1b774be3d3b5f09f76e6cc8:salt:228-129-190-248-134-146-102-97:aes-256-cfb:u+lhtAdW6Re8br0qePqzig== 59 | ``` 60 | 61 | #### Decrypt sample 62 | 63 | ```shell 64 | reversible_cryptography decrypt md5:78e731027d8fd50ed642340b7c9a63b3:salt:252-235-72-88-180-7-195-229:aes-256-cfb:VH2JxqUU9Q== 65 | Input password: 66 | message 67 | ``` 68 | 69 | ```shell 70 | cat encrypted.txt 71 | md5:f5b013aca1b774be3d3b5f09f76e6cc8:salt:228-129-190-248-134-146-102-97:aes-256-cfb:u+lhtAdW6Re8br0qePqzig== 72 | 73 | reversible_cryptography decrypt --password=pass --src-file=encrypted.txt --dst-file=decrypted.txt 74 | 75 | cat decrypted.txt 76 | this is secret! 77 | ``` 78 | 79 | #### Edit sample 80 | ```shell 81 | cat encrypted.txt 82 | md5:f5b013aca1b774be3d3b5f09f76e6cc8:salt:228-129-190-248-134-146-102-97:aes-256-cfb:u+lhtAdW6Re8br0qePqzig== 83 | 84 | reversible_cryptography edit encrypted.txt --password=pass --editor=vim 85 | 86 | # modify encrypted.txt 87 | ``` 88 | 89 | ## Development 90 | 91 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/console` for an interactive prompt that will allow you to experiment. 92 | 93 | 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` to create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 94 | 95 | ## Contributing 96 | 97 | 1. Fork it ( https://github.com/mitaku/reversible_cryptography/fork ) 98 | 2. Create your feature branch (`git checkout -b my-new-feature`) 99 | 3. Commit your changes (`git commit -am 'Add some feature'`) 100 | 4. Push to the branch (`git push origin my-new-feature`) 101 | 5. Create a new Pull Request 102 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "reversible_cryptography" 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 | -------------------------------------------------------------------------------- /bin/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 | -------------------------------------------------------------------------------- /exe/reversible_cryptography: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'reversible_cryptography/cli' 4 | 5 | ReversibleCryptography::CLI.start(ARGV) 6 | -------------------------------------------------------------------------------- /lib/reversible_cryptography.rb: -------------------------------------------------------------------------------- 1 | require "reversible_cryptography/version" 2 | require "reversible_cryptography/message" 3 | 4 | module ReversibleCryptography 5 | class BaseError < StandardError; end 6 | class EmptyInputString < BaseError; end 7 | class InvalidPassword < BaseError; end 8 | class EmptyPassword < InvalidPassword; end 9 | class InvalidFormat < BaseError; end 10 | end 11 | -------------------------------------------------------------------------------- /lib/reversible_cryptography/cli.rb: -------------------------------------------------------------------------------- 1 | require 'thor' 2 | require 'reversible_cryptography' 3 | 4 | module ReversibleCryptography 5 | class CLI < Thor 6 | desc "encrypt [TEXT]", "Encrypt text or file" 7 | option :password, type: :string, aliases: [:p] 8 | option :src_file, type: :string, aliases: [:s], banner: "PLAIN_TEXT_FILE" 9 | option :dst_file, type: :string, aliases: [:d], banner: "ENCRYPTED_TEXT_FILE" 10 | def encrypt(plain_text=nil) 11 | plain_text = File.read(options[:src_file]) if options[:src_file] 12 | plain_text ||= ask("Input text:") 13 | password = options[:password] 14 | password ||= ask("Input password:", echo: false).tap { puts } 15 | 16 | encrypted_text = ReversibleCryptography::Message.encrypt(plain_text, password) 17 | if options[:dst_file] 18 | File.open(options[:dst_file], "wb") do |f| 19 | f.write(encrypted_text) 20 | end 21 | else 22 | puts encrypted_text 23 | end 24 | end 25 | 26 | desc "decrypt [TEXT]", "Decrypt text or file" 27 | option :password, type: :string, aliases: [:p] 28 | option :src_file, type: :string, aliases: [:s], banner: "ENCRYPTED_TEXT_FILE" 29 | option :dst_file, type: :string, aliases: [:d], banner: "PLAIN_TEXT_FILE" 30 | def decrypt(encrypted_text=nil) 31 | encrypted_text = File.read(options[:src_file]) if options[:src_file] 32 | encrypted_text ||= ask("Input text:") 33 | password = options[:password] 34 | password ||= ask("Input password:", echo: false).tap { puts } 35 | 36 | plain_text = ReversibleCryptography::Message.decrypt(encrypted_text, password) 37 | if options[:dst_file] 38 | File.open(options[:dst_file], "wb") do |f| 39 | f.write(plain_text) 40 | end 41 | else 42 | puts plain_text 43 | end 44 | end 45 | 46 | desc "edit FILE", "Edit encrypted file" 47 | option :password, type: :string, aliases: [:p] 48 | option :editor, type: :string, aliases: [:e] 49 | def edit(encrypted_file) 50 | password = options[:password] 51 | password ||= ask("Input password:", echo: false).tap { puts } 52 | editor_command = options[:editor] || ENV['EDITOR'] 53 | 54 | Tempfile.open('decrypted_text') do |fp| 55 | decrypted_text = ReversibleCryptography::Message.decrypt(File.read(encrypted_file), password) 56 | fp.write(decrypted_text) 57 | fp.close 58 | 59 | system("#{editor_command} #{fp.path}") 60 | encrypted_text = ReversibleCryptography::Message.encrypt(File.read(fp.path), password) 61 | 62 | File.open(encrypted_file, "wb") do |f| 63 | f.write(encrypted_text) 64 | end 65 | end 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/reversible_cryptography/message.rb: -------------------------------------------------------------------------------- 1 | require "openssl" 2 | require "base64" 3 | 4 | module ReversibleCryptography 5 | class Message 6 | MODE = "CFB" 7 | PREFIX = "aes-256-cfb" 8 | 9 | class << self 10 | def encrypt(str, password) 11 | raise EmptyInputString if blank?(str) 12 | raise EmptyPassword if blank?(password) 13 | 14 | enc = OpenSSL::Cipher::AES256.new(MODE) 15 | enc.encrypt 16 | 17 | salt = OpenSSL::Random.random_bytes(8) 18 | salt_string = generate_salt_string(salt) 19 | 20 | set_key_and_iv(enc, password, salt) 21 | 22 | result = enc.update(str) + enc.final 23 | 24 | md5 = OpenSSL::Digest.hexdigest("MD5", str) 25 | ["md5", md5, "salt", salt_string, PREFIX, Base64.encode64(result).chomp].join(":") 26 | end 27 | 28 | def decrypt(str, password) 29 | raise EmptyInputString if blank?(str) 30 | raise EmptyPassword if blank?(password) 31 | 32 | key = str.sub(/^md5:(.+):salt:(.+):#{PREFIX}:/, '') 33 | md5 = $1 34 | salt_string = $2 35 | 36 | if [key, md5, salt_string].any? { |s| blank?(s) } 37 | raise InvalidFormat 38 | end 39 | 40 | salt = convert_salt(salt_string) 41 | 42 | dec = OpenSSL::Cipher::AES256.new(MODE) 43 | dec.decrypt 44 | set_key_and_iv(dec, password, salt) 45 | 46 | result = dec.update(Base64.decode64(key)) + dec.final 47 | if OpenSSL::Digest.hexdigest("MD5", result) == md5 48 | result 49 | else 50 | raise InvalidPassword 51 | end 52 | end 53 | 54 | private 55 | 56 | def set_key_and_iv(cipher, password, salt) 57 | key_iv = OpenSSL::PKCS5.pbkdf2_hmac_sha1(password, salt, 2000, cipher.key_len + cipher.iv_len) 58 | cipher.key = key_iv[0, cipher.key_len] 59 | cipher.iv = key_iv[cipher.key_len, cipher.iv_len] 60 | end 61 | 62 | def convert_salt(str) 63 | str.split("-").map(&:to_i).pack("C*") 64 | end 65 | 66 | def generate_salt_string(salt) 67 | salt.unpack("C*").join("-") 68 | end 69 | 70 | def blank?(str) 71 | str.nil? || str.empty? 72 | end 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/reversible_cryptography/version.rb: -------------------------------------------------------------------------------- 1 | module ReversibleCryptography 2 | VERSION = "0.5.0" 3 | end 4 | -------------------------------------------------------------------------------- /reversible_cryptography.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'reversible_cryptography/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "reversible_cryptography" 8 | spec.version = ReversibleCryptography::VERSION 9 | spec.authors = ["Takumi MIURA"] 10 | spec.email = ["mitaku1104@gmail.com"] 11 | 12 | spec.summary = %q{A Símplifìed reversible encryption solution} 13 | spec.description = %q{A Símplifìed reversible encryption solution} 14 | spec.homepage = "https://github.com/mitaku/reversible_cryptography" 15 | 16 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 17 | spec.bindir = "exe" 18 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_dependency "thor" 22 | 23 | spec.add_development_dependency "bundler", "~> 1.8" 24 | spec.add_development_dependency "rake", "~> 10.0" 25 | spec.add_development_dependency "rspec" 26 | end 27 | -------------------------------------------------------------------------------- /spec/reversible_cryptography_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe ReversibleCryptography do 4 | it 'has a version number' do 5 | expect(ReversibleCryptography::VERSION).not_to be nil 6 | end 7 | 8 | shared_examples "raises error on empty input" do 9 | context "When string is empty" do 10 | let(:str) { nil } 11 | let(:password) { "password" } 12 | it { expect{ subject }.to raise_error(ReversibleCryptography::EmptyInputString) } 13 | end 14 | 15 | context "When password is empty" do 16 | let(:str) { "string" } 17 | let(:password) { nil } 18 | it { expect{ subject }.to raise_error(ReversibleCryptography::EmptyPassword) } 19 | end 20 | end 21 | 22 | describe ReversibleCryptography::Message do 23 | describe ".encrypt" do 24 | subject { described_class.encrypt(str, password) } 25 | 26 | let(:str) { "hogehoge" } 27 | let(:password) { "password" } 28 | 29 | it { should match(/^md5:([0-9a-f]+):salt:([0-9-]+):aes-256-cfb:(.+)/) } 30 | 31 | it_behaves_like "raises error on empty input" 32 | end 33 | 34 | describe ".decrypt" do 35 | subject { described_class.decrypt(str, password) } 36 | 37 | let(:str) { "md5:329435e5e66be809a656af105f42401e:salt:131-180-207-255-1-203-171-221:aes-256-cfb:foH0M+/g9UI=" } 38 | 39 | context "valid" do 40 | let(:password) { "piyopiyo" } 41 | it { should eq "hogehoge" } 42 | end 43 | 44 | context "invald" do 45 | let(:password) { "password" } 46 | it { expect { subject }.to raise_error(ReversibleCryptography::InvalidPassword) } 47 | end 48 | 49 | context "When input string is invalid format" do 50 | let(:str) { "deadbeef" } 51 | let(:password) { "hogefuga" } 52 | 53 | it { expect { subject }.to raise_error(ReversibleCryptography::InvalidFormat) } 54 | end 55 | 56 | it_behaves_like "raises error on empty input" 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | require 'reversible_cryptography' 3 | --------------------------------------------------------------------------------