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.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +7 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +62 -0
- data/LICENSE.txt +21 -0
- data/README.md +172 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/rochain +5 -0
- data/lib/record_on_chain/cli.rb +140 -0
- data/lib/record_on_chain/command_loader.rb +20 -0
- data/lib/record_on_chain/commands/abstract_command.rb +55 -0
- data/lib/record_on_chain/commands/help.rb +50 -0
- data/lib/record_on_chain/commands/init.rb +150 -0
- data/lib/record_on_chain/commands/mod_command.rb +38 -0
- data/lib/record_on_chain/commands/record.rb +163 -0
- data/lib/record_on_chain/commands/secret.rb +57 -0
- data/lib/record_on_chain/config.rb +12 -0
- data/lib/record_on_chain/constants.rb +21 -0
- data/lib/record_on_chain/crypto/aes.rb +51 -0
- data/lib/record_on_chain/crypto/cryptor.rb +60 -0
- data/lib/record_on_chain/crypto/default_cryptor.rb +12 -0
- data/lib/record_on_chain/datafile_base.rb +101 -0
- data/lib/record_on_chain/keyfile.rb +14 -0
- data/lib/record_on_chain/nem_controller.rb +113 -0
- data/lib/record_on_chain/utils.rb +44 -0
- data/lib/record_on_chain/version.rb +3 -0
- data/lib/record_on_chain.rb +21 -0
- data/lib/resources/preset_mainnet_nodes +45 -0
- data/lib/resources/preset_testnet_nodes +1 -0
- data/record_on_chain.gemspec +46 -0
- metadata +166 -0
@@ -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,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
|