record_on_chain 0.1.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.
@@ -0,0 +1,57 @@
1
+ require "pathname"
2
+ require_relative "./abstract_command"
3
+ require_relative "./mod_command"
4
+ require_relative "../constants"
5
+ require_relative "../keyfile"
6
+ require_relative "../crypto/default_cryptor"
7
+
8
+ module RecordOnChain
9
+ module Commands
10
+ class Secret < AbstractCommand
11
+
12
+ def self.description
13
+ return "recover secret from keyfile"
14
+ end
15
+
16
+ def self.usage
17
+ output = <<-EOS
18
+ -k <value> => keyfile path ( default : $HOME/.ro_chain/default_key.yml )
19
+
20
+ (e.g.) keyfile_path:/home/user/doc/.ro_chain/my_key.yml
21
+ => $ rochain secret -k /home/user/doc/.ro_chain/my_key.yml
22
+ EOS
23
+ return output
24
+ end
25
+
26
+ def initialize( argv= ARGV , cli= Cli.new )
27
+ super( argv.first , cli )
28
+ val_context = { "-k" => :keyfile_path }
29
+ flag_context = {}
30
+
31
+ args = squeeze_args_from_argv( val_context , flag_context , argv )
32
+
33
+ default_path = Pathname.new( Dir.home ) +
34
+ MAINDIR_NAME +
35
+ D_KEYFILE_NAME ;
36
+ keyfile_path = args[:keyfile_path] ? args[:keyfile_path] : default_path.to_s
37
+
38
+ @keyfile = load_datafile( keyfile_path , "keyfile" )
39
+ rescue => e
40
+ roc_exit( :halt , "#{e.message}" )
41
+ end
42
+
43
+ def start
44
+ secret = get_secret( @cli, @keyfile )
45
+ msg = "Secret [ #{secret} ]"
46
+ roc_exit( :nomal_end , msg )
47
+ rescue => e
48
+ roc_exit( :halt , "#{e.message}" )
49
+ end
50
+
51
+ private
52
+
53
+ include M_LoadDatafile
54
+ include M_GetSecret
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,12 @@
1
+ require_relative "./datafile_base"
2
+
3
+ module RecordOnChain
4
+ class Config
5
+ extend DatafileBase
6
+ define_datafile_class(
7
+ { :var => :keyfile_path , :type => String },
8
+ { :var => :recipient , :type => String },
9
+ { :var => :add_node , :type => Array }
10
+ )
11
+ end
12
+ end
@@ -0,0 +1,21 @@
1
+ module RecordOnChain
2
+ # region generally
3
+ MAINDIR_NAME = ".ro_chain".freeze
4
+
5
+ # region crypto
6
+ SECRET_LENGTH = 32.freeze
7
+ SALT_LENGTH = 16.freeze
8
+ CHECKSUM_LENGTH = 4.freeze
9
+
10
+ # region datafile
11
+ D_DATAFILE_NAME = "default".freeze
12
+ D_KEYFILE_SUFFIX = "_key.yml".freeze
13
+ D_CONFIGFILE_SUFFIX = "_config.yml".freeze
14
+
15
+ D_KEYFILE_NAME = ( D_DATAFILE_NAME + D_KEYFILE_SUFFIX ).freeze
16
+ D_CONFIGFILE_NAME = ( D_DATAFILE_NAME + D_CONFIGFILE_SUFFIX ).freeze
17
+
18
+ # region dirpath
19
+ COMMANDS_DIRPATH = File.expand_path( "../commands" , __FILE__ ).freeze
20
+ RESOURCES_DIRPATH = File.expand_path( "../../resources" , __FILE__ ).freeze
21
+ end
@@ -0,0 +1,51 @@
1
+ # reference
2
+ # https://docs.ruby-lang.org/ja/latest/class/OpenSSL=3a=3aCipher.html
3
+ require 'openssl'
4
+
5
+ module RecordOnChain
6
+ module Crypto
7
+ class AES
8
+ def encrypt( passwd, salt, secret )
9
+ return crypto_func_base( passwd, salt, secret, :encrypt )
10
+ end
11
+
12
+ def decrypt( passwd, salt, encrypted_data )
13
+ return crypto_func_base( passwd, salt, encrypted_data, :decrypt )
14
+ end
15
+
16
+ private
17
+ def generate_base
18
+ return OpenSSL::Cipher.new("AES-256-CBC")
19
+ end
20
+
21
+ def calc_key_and_initvector( passwd, salt )
22
+ aes = generate_base
23
+ # key_inv = key( aes.key_len byte ) | inv ( aes.iv_len byte )
24
+ key_iv = OpenSSL::PKCS5.pbkdf2_hmac_sha1( passwd, salt, 2000, aes.key_len + aes.iv_len )
25
+ key = key_iv[ 0,aes.key_len ]
26
+ iv = key_iv[ aes.key_len, aes.iv_len ]
27
+ return { key: key, initvector: iv }
28
+ end
29
+
30
+ def set_key_iv( aes, passwd, salt )
31
+ key_iv = calc_key_and_initvector( passwd, salt )
32
+ aes.key = key_iv[ :key ]
33
+ aes.iv = key_iv[ :initvector ]
34
+ end
35
+
36
+ #@param [passwd] passwd
37
+ #@param [salt] salt
38
+ #@param [data] :encrypt => secret_data , decrypt => encrypted_data
39
+ #@param [encrypt_or_decrypt] :encrypt or :decrypt (symbol)
40
+ def crypto_func_base( passwd, salt, data, encrypt_or_decrypt )
41
+ aes = generate_base
42
+ aes.send( encrypt_or_decrypt )
43
+ set_key_iv( aes, passwd, salt )
44
+ output = ""
45
+ output << aes.update( data )
46
+ output<< aes.final
47
+ return output
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,60 @@
1
+ require "digest/md5"
2
+ require_relative '../utils'
3
+ require_relative '../constants'
4
+
5
+ module RecordOnChain
6
+ module Crypto
7
+ class Cryptor
8
+ def initialize( crypto_engine )
9
+ @crypto_engine = crypto_engine
10
+ end
11
+
12
+ def generate_secret
13
+ return SecureRandom.hex( SECRET_LENGTH )
14
+ end
15
+
16
+ def generate_salt
17
+ return SecureRandom.hex( SALT_LENGTH )
18
+ end
19
+
20
+ def encrypt( passwd, hex_salt, hex_secret )
21
+ b_secret = Utils.hex_to_bytes( hex_secret )
22
+ # add checksum to secret
23
+ b_secret << calc_checksum( b_secret )
24
+ b_salt = Utils.hex_to_bytes( hex_salt )
25
+ encrypted = @crypto_engine.encrypt( passwd, b_salt, b_secret )
26
+ return Utils.bytes_to_hex( encrypted )
27
+ end
28
+
29
+ def decrypt( passwd, hex_salt, hex_encrypted_data )
30
+ b_data = Utils.hex_to_bytes( hex_encrypted_data )
31
+ b_salt = Utils.hex_to_bytes( hex_salt )
32
+ decrypted = ""
33
+ begin
34
+ # At this point it is not known whether the decrypted data is correct or not.
35
+ # You should verify decrypted data with checksum.
36
+ decrypted = @crypto_engine.decrypt( passwd, b_salt, b_data )
37
+ rescue OpenSSL::Cipher::CipherError
38
+ # fail to encrypt
39
+ return ""
40
+ end
41
+ # validate checksum
42
+ secret_length = decrypted.size - CHECKSUM_LENGTH
43
+ secret_part = decrypted[0,secret_length]
44
+ checksum_part = decrypted[secret_length..-1]
45
+ checksum = calc_checksum( secret_part )
46
+ # not match checksum
47
+ return "" unless checksum_part == checksum
48
+ # match checksum
49
+ return Utils.bytes_to_hex( secret_part )
50
+ end
51
+
52
+ private
53
+
54
+ def calc_checksum( data )
55
+ # MD5 Hash [0..CCHECKSUM_LENGTH]
56
+ return Digest::MD5.digest( data )[ 0 , CHECKSUM_LENGTH ]
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,12 @@
1
+ require_relative "./aes"
2
+ require_relative "./cryptor"
3
+
4
+ module RecordOnChain
5
+ module Crypto
6
+ class DefaultCryptor
7
+ def self.generate
8
+ return Crypto::Cryptor.new( Crypto::AES.new )
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,101 @@
1
+ require "yaml"
2
+ require_relative "./utils"
3
+
4
+ module RecordOnChain
5
+ module DatafileBase
6
+ # status_context = { :var => variable_name , :type => variable_type }
7
+ def define_datafile_class(*status_context)
8
+
9
+ variables = status_context.map{ |c| c[:var] }
10
+
11
+ # [ presudo code ]
12
+ # def initialize( args )
13
+ # @status1 = args[1]
14
+ # @status2 = args[2]
15
+ # ...
16
+ # end
17
+ define_method(:initialize) do |*args|
18
+ variables.each_with_index do |var , index|
19
+ # add "@" to sym
20
+ # [e.g.] { var: :name , type: String } => :name => :@name
21
+ var_name = ( "@#{var}" ).to_sym
22
+ instance_variable_set( var_name, args[index] )
23
+ end
24
+ end
25
+
26
+ attr_reader *variables
27
+
28
+ # [ presudo code ]
29
+ # def to_yml
30
+ # hash = [ status1: @status1, status2: @status2 ... ]
31
+ # return hash.to_yml
32
+ # end
33
+ define_method(:to_yml) do
34
+ hash = {}
35
+ instance_variables.each do |var_sym|
36
+ # delete "@" from instance_variables sym
37
+ # [e.g.] :@status1 => :status1
38
+ key = var_sym.to_s.delete("@").to_sym
39
+ # hash[ :status1 ] = @status1
40
+ hash[key] = instance_variable_get( var_sym )
41
+ end
42
+ return hash.to_yaml
43
+ end
44
+
45
+ # [ presudo code ]
46
+ # self.def generate( filepath , *args )
47
+ # obj = Datafile.new( *args )
48
+ # yml = obj.to_yml
49
+ # File.open( filepath, "w" ){ |f| f.write( yml ) }
50
+ # end
51
+ singleton_class.send( :define_method , :generate ) do |filepath , *args|
52
+ datafile_obj = new( *args )
53
+ yml = datafile_obj.to_yml
54
+ File.open( filepath , "w" ){ |f| f.write( yml ) }
55
+ end
56
+
57
+ # [ presudo code ]
58
+ # def self.load( filepath )
59
+ # hash = YAML.load_file( filepath )
60
+ # return from_hash( hash )
61
+ # end
62
+ singleton_class.send( :define_method , :load ) do |filepath|
63
+ hash = YAML.load_file( filepath )
64
+ # RecordOnChain::Utils
65
+ hash = Utils.symbolize_hashkeys_rf( hash )
66
+ return from_hash(hash)
67
+ end
68
+
69
+ # [ presudo code ]
70
+ # def self.from_hash( hash )
71
+ #
72
+ # types = { status1: String , status2: String , status3: Integer ,...}
73
+ #
74
+ # return nil if hash[ :status1 ].nil? || hash[ :status1 ].class != types[ :status1 ]
75
+ # @status1 = hash[;status1]
76
+ #
77
+ # return nil if hash[ :status2 ].nil?|| hash[ :status2 ].class != types[ :status2 ]
78
+ # @status2 = hash[;status2]
79
+ #
80
+ # return nil if hash[ :status3 ].nil?|| hash[ :status3 ].class != types[ :status3 ]
81
+ # @status3 = hash[;status3]
82
+ # ...
83
+ # end
84
+ singleton_class.send( :define_method , :from_hash ) do |hash|
85
+ args = []
86
+
87
+ types = status_context.map{ |c| c[:type] }
88
+
89
+ variables.each_with_index do |var,index|
90
+ value = hash[ var ]
91
+ # If there is missing status in hash, return nil without generating the object
92
+ return nil if value.nil?
93
+ # If type of field does not match type of context, return nil without generating the object
94
+ return nil unless value.class == types[ index ]
95
+ args.push( value )
96
+ end
97
+ return new(*args)
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,14 @@
1
+ require_relative "./datafile_base"
2
+
3
+ module RecordOnChain
4
+ class Keyfile
5
+ extend DatafileBase
6
+ define_datafile_class(
7
+ { :var => :network_type , :type => String },
8
+ { :var => :salt , :type => String },
9
+ { :var => :encrypted_secret , :type => String },
10
+ { :var => :public_key , :type => String },
11
+ { :var => :address , :type => String }
12
+ )
13
+ end
14
+ end
@@ -0,0 +1,113 @@
1
+ require "nem"
2
+
3
+ module RecordOnChain
4
+ class NemController
5
+ @@NET_TYPES = [:testnet , :mainnet].freeze
6
+
7
+ def self.public_from_secret( secret )
8
+ return Nem::Keypair.new( secret ).public
9
+ end
10
+
11
+ def self.address_from_public( public_key , network_type )
12
+ return Nem::Unit::Address.from_public_key( public_key , network_type.to_sym ).to_s
13
+ end
14
+
15
+ def self.address_from_secret( secret , network_type )
16
+ public_key = public_from_secret( secret )
17
+ return address_from_public( public_key , network_type.to_sym )
18
+ end
19
+
20
+ def initialize( node_url_set , network_type = :testnet )
21
+ raise ArgumentError,"Node set must not be empty." if node_url_set.empty?
22
+ raise ArgumentError,"Unknown network type.[:testnet,:mainnet]" unless @@NET_TYPES.include?(network_type.to_sym)
23
+
24
+ # make node_pool from node_set
25
+ node_objects = []
26
+ node_url_set.each do |url|
27
+ node_object = Nem::Node.new( url: url )
28
+ node_objects.push( node_object )
29
+ end
30
+ @node_pool = Nem::NodePool.new( node_objects )
31
+ @network_type = network_type.to_sym
32
+ end
33
+
34
+ def get_address_status( address )
35
+ endpoint = Nem::Endpoint::Account.new( @node_pool )
36
+ add = endpoint.find( address )
37
+ multisig = !add.cosignatories.empty?
38
+ return { balance: add.balance , multisig: multisig }
39
+ end
40
+
41
+ def prepare_tx( recipient_str, msg )
42
+ check_address( recipient_str )
43
+ modified_address = modify_address( recipient_str )
44
+ # NOTE: Timestamp is set to -10 sec from Time.now.It prevent FAILURE_TIMESTAMP_TOO_FAR_IN_FUTURE
45
+ @tx = Nem::Transaction::Transfer.new( modified_address, 0, msg, timestamp: Time.now() -10, network: @network_type )
46
+ end
47
+
48
+ def calc_fee
49
+ prepared?
50
+ fee = Nem::Fee::Transfer.new( @tx )
51
+ return fee.to_i
52
+ end
53
+
54
+ def send_transfer_tx( private_key )
55
+ prepared?
56
+ result = broadcast_transfer_tx( private_key )
57
+ return result
58
+ end
59
+
60
+ # default log path : stdout
61
+ def set_log_path( dir_path )
62
+ log_name = "nem_controller.log"
63
+ Nem.logger = Logger.new( dir_path + log_name )
64
+ end
65
+
66
+ private
67
+
68
+ def prepared?
69
+ raise "Error : Please prepare tx before getting fee." if @tx.nil?
70
+ end
71
+
72
+ # delete sigins & upcase
73
+ def modify_address( address )
74
+ return address.gsub( /\W/ , "" ).upcase
75
+ end
76
+
77
+ # check whether address matches network_type
78
+ def check_address( recipient_str )
79
+ initial = recipient_str.upcase[0]
80
+ error_msg = "Illegal address. You should make sure address and network type."
81
+ case initial
82
+ when "T" then
83
+ raise ArgumentError,error_msg unless @network_type == :testnet
84
+ when "N" then
85
+ raise ArgumentError,error_msg unless @network_type == :mainnet
86
+ else
87
+ raise ArgumentError,error_msg
88
+ end
89
+ end
90
+
91
+ def broadcast_transfer_tx( sender_privatekey )
92
+ sender_keypair = Nem::Keypair.new( sender_privatekey )
93
+ announce_request = Nem::Request::Announce.new( @tx, sender_keypair )
94
+ endpoint = Nem::Endpoint::Transaction.new( @node_pool )
95
+ response = {}
96
+ begin
97
+ # If node pool has next node, it will automatically retry sending transaction.
98
+ response = endpoint.announce( announce_request )
99
+ rescue => e
100
+ # all node failure
101
+ return { success?: false, message: e.message, error_type: e.class.to_s }
102
+ end
103
+ # response.code == 1 ~> success
104
+ # rasponse.code != 1 -> something failure
105
+ case response.code
106
+ when 1 then
107
+ return { success?: true, message: response.message, tx_hash: response.transaction_hash }
108
+ else
109
+ return { success?: false, message: response.message, error_type: e.class.to_s }
110
+ end
111
+ end
112
+ end
113
+ end