itsy-btc 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 6cc6b7da4cccd2c198f421508ea9f27b44b6015e
4
+ data.tar.gz: 5296961199659fb87256ce91015965ad16111668
5
+ SHA512:
6
+ metadata.gz: 9b62525545c12765e7ecd10fbe1382d1a98c6b325d7588f385c7b917590bed6e614b8e449808de91753b5f52245cd8bafd0ec9d3097e88b69fc5f40cdc4a500e
7
+ data.tar.gz: b19b4673fdcaa634a68933265cf5f3579f0cba67979ab4c5bdfb3db64c14be2090b9d87929ce1219b928129a1472935e9b221c1a2ca30406db7884006f2c7a11
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source :rubygems
2
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,21 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ itsy-btc (0.0.1)
5
+ bitcoin-ruby
6
+ bundler
7
+ highline
8
+ open4
9
+
10
+ GEM
11
+ remote: http://rubygems.org/
12
+ specs:
13
+ bitcoin-ruby (0.0.2)
14
+ highline (1.6.20)
15
+ open4 (1.3.0)
16
+
17
+ PLATFORMS
18
+ ruby
19
+
20
+ DEPENDENCIES
21
+ itsy-btc!
data/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2013 Jeremy Ruten
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,108 @@
1
+ # itsy-btc
2
+
3
+ `itsy` is a funny little bitcoin wallet for people who can't trust their computers to keep a secret, especially when their *money* exists entirely as a collection of secrets. It also feels a lot like using git, where transactions are like commits that you create, sign, and then push to the distributed repository, i.e. the blockchain. If you like git, then you will feel right at home with this tool.
4
+
5
+ ## Rationale
6
+
7
+ Here's the main problem this tool tries to solve: You likely have a computer that is years old, and over those many years you have downloaded and run countless executables from the internet, many of which themselves run on your computer and accept all sorts of connections to your computer from anyone in the world. You have to assume there are vulnerabilities in some of the code you have ran during your computer's history, and so you really can't assume that there isn't some ~~bad~~ code running that is inspecting your computer's memory for secrets and broadcasting them periodically to ~~bad~~ people.
8
+
9
+ Now it does seem rather unlikely to me that my keys are being logged, or my screen is being shot, or my RAM is being scanned, for purposes of skimming bitcoin private keys. But I think it's good to be paranoid about the possibility of my secrets being stolen even if their unencrypted form only exists in RAM for short amounts of time. As bitcoin gets more popular, these kinds of attacks will probably become common. (Probably mostly by frustrated miners who can no longer afford the hardware to keep up, so they have to "mine" their precious bitcoins in more creative ways...) Another thing to worry about is all the intentional backdoors at the OS or hardware level that have been uncovered lately, through which who-knows-how-many employees could get into your computer and swipe some secrets.
10
+
11
+ So the only solution I see is to create and manage all your secrets on a computer that is permanently offline. A nice cheap, convenient solution is to use a Raspberry Pi for this.
12
+
13
+ ## Install
14
+
15
+ Install the `itsy-btc` gem on your online, every-day computer:
16
+
17
+ $ gem install itsy-btc
18
+
19
+ Then you want to install it on your offline computer as well. Simply fetch the gem file and its dependencies and transfer them to your offline computer with a USB stick or similar, and install:
20
+
21
+ $ gem fetch itsy-btc bitcoin-ruby ffi highline
22
+ $ cp *.gem /mnt/usb/
23
+ ... switch computers ...
24
+ $ cd /mnt/usb
25
+ $ gem install itsy-btc-0.0.1.gem
26
+
27
+ ## Basic usage
28
+
29
+ Most commands operate on a file called WALLET.itsy that is in your current directory. To create this file, use the `init` command:
30
+
31
+ $ itsy init
32
+
33
+ It will ask what passphrase to use to encrypt the wallet with. Make it a good one!
34
+
35
+ Now generate an address:
36
+
37
+ $ itsy gen "optional comment"
38
+
39
+ You can generate as many as you want. List them like this:
40
+
41
+ $ itsy list
42
+
43
+ To make transactions, your online computer needs to know what all your addresses are, so copy your wallet file to your online machine. Your private keys are encrypted in this file, so as long as you never type your passphrase on your online computer, they should be safe.
44
+
45
+ $ cp WALLET.itsy /mnt/usb/
46
+
47
+ Then on your online machine, after you've received money into your address, you can create a transaction to send some money to someone:
48
+
49
+ $ cd /mnt/usb
50
+ $ ls
51
+ WALLET.itsy
52
+ $ itsy tx 0.25 18wh2CbAjeB7i1w3zjPX8iCasy1zMYYgnv
53
+ Will send 0.25 BTC to 18wh2CbAjeB7i1w3zjPX8iCasy1zMYYgnv with a fee of 0.0001 BTC.
54
+ Transaction saved to 8b1738aaadb37a6c51d9caeec31a0b4f6bb0e0aa6ef7346a36a2813437298964.json.
55
+
56
+ It figures out what inputs to use using information downloaded from blockexplorer.com.
57
+
58
+ Okay, you have the transaction as a json file on your USB device. Now switch to your offline computer to sign the transaction:
59
+
60
+ $ itsy sign 8b1738aaadb37a6c51d9caeec31a0b4f6bb0e0aa6ef7346a36a2813437298964.json
61
+ 1 input signed, transaction saved to 8b1738aaadb37a6c51d9caeec31a0b4f6bb0e0aa6ef7346a36a2813437298964-signed.json.
62
+
63
+ Finally, transfer this new file to your online computer and push the signed transaction to the block chain:
64
+
65
+ $ itsy push 8b1738aaadb37a6c51d9caeec31a0b4f6bb0e0aa6ef7346a36a2813437298964-signed.json
66
+ Transaction pushed successfully.
67
+
68
+ ## Advanced usage
69
+
70
+ Create custom transaction:
71
+
72
+ $ itsy tx
73
+ (1) Add input, (2) Add output, (3) Save, or (4) Cancel? 1
74
+ Prev tx? f480a09eb541c27fa4d3b7f3d3fceabde50840758248327fd0fb3cc6970c7486
75
+ Output index? 0
76
+ Input added. (Amount: 0.25 BTC)
77
+ (1) Add input, (2) Add output, (3) Save, or (4) Cancel? 2
78
+ Address? 1B2p7d1BPMqVrQMb97kTk5RbUvUH9CetBK
79
+ Amount? 0.1
80
+ Output added.
81
+ (1) Add input, (2) Add output, (3) Save, or (4) Cancel? 2
82
+ Address? 1C19FTsSj2ahsRjGfRFuiyDm9QJg9UZVSJ
83
+ Amount? 0.1499
84
+ Output added.
85
+ (1) Add input, (2) Add output, (3) Save, or (4) Cancel? 3
86
+ Will send 0.1 BTC from 1 input to 1B2p7d1BPMqVrQMb97kTk5RbUvUH9CetBK with a 0.0001 BTC fee.
87
+ Transaction saved to 9e62c79d41670168e11e98f68ed709ea22004a73978c37384e7d9613e5e2961e.json.
88
+
89
+ Authenticate to access encrypted wallet during a session (otherwise it will ask for your key every time you sign a transaction or modify your wallet, which might be okay):
90
+
91
+ $ itsy auth
92
+ Your passphrase? mysup3rs3cretpassw0rd
93
+ Wallet unlocked until poweroff.
94
+
95
+ ## TODO
96
+
97
+ * Unit tests
98
+ * Tighten error checking, integrity checks all over, i.e. make it hard for users to do anything stupid
99
+ * Document commands thoroughly, so `itsy help <command>` gives lots of info for each command
100
+ * Experiment with interacting with the bitcoin network directly (with bitcoin-ruby) to remove some of the dependency on blockexplorer.com (especially for the `push` command)
101
+
102
+ * Idea: maybe wallet should be partitioned into hot and cold storage, with simple ways of transferring one to the other. Then you can have the best of both worlds.
103
+
104
+ ## Warning
105
+
106
+ This project is just me trying to do bitcoin the way I like to do things. I'm actually very new and inexperienced in bitcoin, crypto, and security. So read all the code and understand how it works before using it in any serious way, please.
107
+
108
+
data/bin/itsy ADDED
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/ruby
2
+
3
+ require "itsy-btc"
4
+
5
+ if ARGV.first == "test"
6
+ ItsyBtc.test_mode!
7
+ ARGV.shift
8
+ end
9
+
10
+ WALLET_PATH = ItsyBtc::DEFAULT_WALLET_FILENAME
11
+
12
+ wallet = nil
13
+ if File.exists?(WALLET_PATH)
14
+ wallet = ItsyBtc::Wallet.new(WALLET_PATH)
15
+ end
16
+
17
+ if ARGV.empty?
18
+ puts "Yes, that's my name. Now what do you want?"
19
+ puts
20
+ command_name = "help"
21
+ else
22
+ command_name = ARGV.shift.downcase
23
+ end
24
+
25
+ command_class = ItsyBtc::Commands::LIST.find { |cmd| cmd.name == command_name }
26
+
27
+ if command_class
28
+ begin
29
+ command = command_class.new(*ARGV)
30
+ command.wallet = wallet
31
+ command.run
32
+ rescue OpenSSL::Cipher::CipherError
33
+ puts "Wrong passphrase! Aborting..."
34
+ exit
35
+ end
36
+ else
37
+ puts "Unrecognized command: #{command_name}"
38
+ puts "Type `itsy help` for help."
39
+ exit
40
+ end
41
+
data/itsy-btc.gemspec ADDED
@@ -0,0 +1,21 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "itsy-btc"
3
+ s.version = "0.0.1"
4
+ s.date = "2013-11-14"
5
+ s.summary = "A simple secure bitcoin wallet."
6
+ s.description = "itsy-btc is a simple secure bitcoin wallet that allows you to turn an offline computer into a hardware wallet."
7
+ s.author = "Jeremy Ruten"
8
+ s.email = "jeremy.ruten@gmail.com"
9
+ s.homepage = "http://github.com/yjerem/itsy-btc"
10
+ s.license = "MIT"
11
+ s.required_ruby_version = ">= 1.9.2"
12
+
13
+ s.files = ["Gemfile", "Gemfile.lock", "LICENSE", "itsy-btc.gemspec", "README.md"]
14
+ s.files += Dir["lib/**/*.rb"]
15
+ s.files += Dir["bin/itsy"]
16
+ s.executables = ["itsy"]
17
+
18
+ %w(bundler bitcoin-ruby ffi highline).each do |gem_name|
19
+ s.add_runtime_dependency gem_name
20
+ end
21
+ end
data/lib/itsy-btc.rb ADDED
@@ -0,0 +1,38 @@
1
+ require "bitcoin"
2
+ require "open-uri"
3
+ require "highline/import"
4
+ require "bigdecimal"
5
+ require "json"
6
+
7
+ require "itsy-btc/wallet"
8
+ require "itsy-btc/wallet_entry"
9
+ require "itsy-btc/lookup"
10
+ require "itsy-btc/commands"
11
+
12
+ module ItsyBtc
13
+ DEFAULT_WALLET_FILENAME = "WALLET.itsy"
14
+
15
+ class << self
16
+ def format_value(satoshis)
17
+ (BigDecimal.new(satoshis) / 1e8).to_s("F")
18
+ end
19
+
20
+ def blockexplorer_url
21
+ if test_mode?
22
+ "http://blockexplorer.com/testnet"
23
+ else
24
+ "http://blockexplorer.com"
25
+ end
26
+ end
27
+
28
+ def test_mode?
29
+ @test_mode
30
+ end
31
+
32
+ def test_mode!
33
+ @test_mode = true
34
+ Bitcoin.network = :testnet
35
+ end
36
+ end
37
+ end
38
+
@@ -0,0 +1,37 @@
1
+ require "itsy-btc/commands/command"
2
+ require "itsy-btc/commands/init_command"
3
+ require "itsy-btc/commands/gen_command"
4
+ require "itsy-btc/commands/import_command"
5
+ require "itsy-btc/commands/delete_command"
6
+ require "itsy-btc/commands/balance_command"
7
+ require "itsy-btc/commands/list_command"
8
+ require "itsy-btc/commands/comment_command"
9
+ require "itsy-btc/commands/tx_command"
10
+ require "itsy-btc/commands/sign_command"
11
+ require "itsy-btc/commands/verify_command"
12
+ require "itsy-btc/commands/push_command"
13
+ require "itsy-btc/commands/decrypt_command"
14
+ require "itsy-btc/commands/passwd_command"
15
+ require "itsy-btc/commands/help_command"
16
+
17
+ module ItsyBtc
18
+ module Commands
19
+ LIST = [
20
+ InitCommand,
21
+ GenCommand,
22
+ ImportCommand,
23
+ DeleteCommand,
24
+ BalanceCommand,
25
+ ListCommand,
26
+ CommentCommand,
27
+ TxCommand,
28
+ SignCommand,
29
+ VerifyCommand,
30
+ PushCommand,
31
+ DecryptCommand,
32
+ PasswdCommand,
33
+ HelpCommand
34
+ ]
35
+ end
36
+ end
37
+
@@ -0,0 +1,24 @@
1
+ module ItsyBtc
2
+ module Commands
3
+ class BalanceCommand < Command
4
+ name "balance"
5
+ summary "show your total balance"
6
+
7
+ def run
8
+ if @wallet.nil?
9
+ puts "No wallet found! Use `itsy init` to create one."
10
+ exit
11
+ end
12
+
13
+ entries = @wallet.entries
14
+ if entries.empty?
15
+ puts "This wallet has no addresses."
16
+ else
17
+ total = entries.map { |e| Lookup.balance(e.address) || exit }.inject(:+)
18
+ puts ItsyBtc.format_value(total) + " BTC"
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+
@@ -0,0 +1,27 @@
1
+ module ItsyBtc
2
+ module Commands
3
+ class Command
4
+ attr_accessor :wallet
5
+
6
+ def run
7
+ raise NotImplementedError, "command not implemented!"
8
+ end
9
+
10
+ def self.name(value=nil)
11
+ @name = value if value
12
+ @name
13
+ end
14
+
15
+ def self.args(value=nil)
16
+ @args = value if value
17
+ @args
18
+ end
19
+
20
+ def self.summary(value=nil)
21
+ @summary = value if value
22
+ @summary
23
+ end
24
+ end
25
+ end
26
+ end
27
+
@@ -0,0 +1,36 @@
1
+ module ItsyBtc
2
+ module Commands
3
+ class CommentCommand < Command
4
+ name "comment"
5
+ args "<address> <new comment>"
6
+ summary "edit an address's comment field"
7
+
8
+ def initialize(address, comment)
9
+ @address = address.to_s
10
+ @comment = comment.to_s
11
+ end
12
+
13
+ def run
14
+ if @wallet.nil?
15
+ puts "No wallet found. Run `itsy init` to create one."
16
+ exit
17
+ end
18
+
19
+ entries = @wallet.addresses_starting_with(@address)
20
+ if entries.length == 1
21
+ address = entries.first.address
22
+ @wallet.update_comment(address, @comment)
23
+ puts "Comment for #{address} updated."
24
+ elsif entries.length == 0
25
+ puts "No addresses in your wallet start with '#{@address}'."
26
+ else
27
+ puts "Multiple addresses in your wallet start with '#{@address}':"
28
+ entries.each do |entry|
29
+ puts " #{entry.address} #{entry.comment}"
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+
@@ -0,0 +1,23 @@
1
+ module ItsyBtc
2
+ module Commands
3
+ class DecryptCommand < Command
4
+ name "decrypt"
5
+ summary "show all of your private keys"
6
+
7
+ def run
8
+ unless @wallet
9
+ puts "No wallet found! Use `itsy init` to create one."
10
+ exit
11
+ end
12
+
13
+ @wallet.entries_with_keys.each do |entry|
14
+ puts "addr: #{entry.address}"
15
+ puts "key: #{entry.key}"
16
+ puts "comment: #{entry.comment}"
17
+ puts
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+
@@ -0,0 +1,29 @@
1
+ module ItsyBtc
2
+ module Commands
3
+ class DeleteCommand < Command
4
+ name "delete"
5
+ args "<address>"
6
+ summary "delete an address permanently"
7
+
8
+ def initialize(address)
9
+ @address = address.to_s
10
+ end
11
+
12
+ def run
13
+ if @wallet.nil?
14
+ puts "No wallet found. Run `itsy init` to create one."
15
+ exit
16
+ end
17
+
18
+ key = @wallet.key_for_address(@address)
19
+ if @wallet.remove(@address)
20
+ puts "Address #{@address} deleted."
21
+ puts "Here is the key in case you made a mistake: #{key}"
22
+ else
23
+ puts "There is no address #{@address} in your wallet."
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+
@@ -0,0 +1,27 @@
1
+ module ItsyBtc
2
+ module Commands
3
+ class GenCommand < Command
4
+ name "gen"
5
+ args "[comment]"
6
+ summary "generate an address"
7
+
8
+ def initialize(comment = "")
9
+ @comment = comment.to_s
10
+ end
11
+
12
+ def run
13
+ if @wallet.nil?
14
+ puts "No wallet found. Run `itsy init` to create one."
15
+ exit
16
+ end
17
+
18
+ key_gen = Bitcoin::Wallet::KeyGenerator.new
19
+ key = key_gen.get_key
20
+ @wallet.add(key.to_base58, @comment)
21
+
22
+ puts "Address #{@wallet.entries.last.address} added to wallet."
23
+ end
24
+ end
25
+ end
26
+ end
27
+
@@ -0,0 +1,22 @@
1
+ module ItsyBtc
2
+ module Commands
3
+ class HelpCommand < Command
4
+ name "help"
5
+ summary "print this summary"
6
+
7
+ SPACING = 40
8
+
9
+ def run
10
+ puts "Usage: itsy <command> [args...]"
11
+ puts
12
+ puts "Available commands:"
13
+ ItsyBtc::Commands::LIST.each do |cmd_class|
14
+ spacing = " " * (SPACING - cmd_class.name.length - 1 - cmd_class.args.to_s.length)
15
+ puts " #{cmd_class.name} #{cmd_class.args}#{spacing}#{cmd_class.summary}"
16
+ end
17
+ puts
18
+ end
19
+ end
20
+ end
21
+ end
22
+
@@ -0,0 +1,63 @@
1
+ module ItsyBtc
2
+ module Commands
3
+ class ImportCommand < Command
4
+ name "import"
5
+ args "[path]"
6
+ summary "import an address from base58"
7
+
8
+ def initialize(path = nil)
9
+ @path = path
10
+ end
11
+
12
+ def run
13
+ if @wallet.nil?
14
+ puts "No wallet found. Run `itsy init` to create one."
15
+ exit
16
+ end
17
+
18
+ f = File.open(@path, "r") if @path
19
+
20
+ num_imports = 0
21
+ done = false
22
+ while not done
23
+ if @path
24
+ line = f.readline
25
+ base58, *comment = line.split
26
+ base58 = base58.to_s
27
+ comment = comment.join(" ")
28
+ else
29
+ base58 = ask("Base58 key? ").to_s
30
+ comment = ask("Comment? (optional) ").to_s
31
+ end
32
+
33
+ key = begin
34
+ Bitcoin::Key.from_base58(base58)
35
+ rescue
36
+ puts "Key '#{base58}' is invalid!" unless base58.empty? && @path
37
+ nil
38
+ end
39
+
40
+ if key
41
+ begin
42
+ @wallet.add(key.to_base58, comment)
43
+ num_imports += 1
44
+ rescue DuplicateAddressError
45
+ puts "Address '#{key.addr}' already exists in your wallet!"
46
+ end
47
+ end
48
+
49
+ if @path
50
+ done = f.eof?
51
+ else
52
+ done = ask("Import another key? [Y/n] ").to_s.downcase == "n"
53
+ end
54
+ end
55
+
56
+ f.close if @path
57
+
58
+ puts "#{num_imports} addresses added to wallet."
59
+ end
60
+ end
61
+ end
62
+ end
63
+
@@ -0,0 +1,25 @@
1
+ module ItsyBtc
2
+ module Commands
3
+ class InitCommand < Command
4
+ name "init"
5
+ summary "create new wallet"
6
+
7
+ def run
8
+ path = ItsyBtc::DEFAULT_WALLET_FILENAME
9
+ if File.exists?(path)
10
+ puts "A '#{path}' file already exists in this directory!"
11
+ else
12
+ passphrase1 = ask("Choose wallet passphrase: ") { |q| q.echo = false }
13
+ passphrase2 = ask("Verify your passphrase: ") { |q| q.echo = false }
14
+ if passphrase1 != passphrase2
15
+ puts "You typed different passphrases! Aborting..."
16
+ else
17
+ Wallet.new(path).create(passphrase1)
18
+ puts "'#{path}' created."
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+
@@ -0,0 +1,26 @@
1
+ module ItsyBtc
2
+ module Commands
3
+ class ListCommand < Command
4
+ name "list"
5
+ summary "list your addresses"
6
+
7
+ def run
8
+ if @wallet.nil?
9
+ puts "No wallet found! Use `itsy init` to create one."
10
+ exit
11
+ end
12
+
13
+ entries = @wallet.entries
14
+ if entries.empty?
15
+ puts "This wallet is empty."
16
+ else
17
+ puts "Address Comment"
18
+ entries.each do |entry|
19
+ puts "#{entry.address} #{entry.comment}"
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+
@@ -0,0 +1,29 @@
1
+ module ItsyBtc
2
+ module Commands
3
+ class PasswdCommand < Command
4
+ name "passwd"
5
+ summary "change your wallet passphrase"
6
+
7
+ def run
8
+ if @wallet.nil?
9
+ puts "No wallet found. Run `itsy init` to create one."
10
+ exit
11
+ end
12
+
13
+ old_passphrase = ask("Old wallet passphrase? ") { |q| q.echo = false }
14
+ @wallet.verify_passphrase(old_passphrase)
15
+
16
+ new_passphrase1 = ask("New wallet passphrase? ") { |q| q.echo = false }
17
+ new_passphrase2 = ask("Verify passphrase? ") { |q| q.echo = false }
18
+
19
+ if new_passphrase1 != new_passphrase2
20
+ puts "You typed different passphrases! Aborting..."
21
+ else
22
+ @wallet.change_passphrase(new_passphrase1)
23
+ puts "Passphrase changed."
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+
@@ -0,0 +1,30 @@
1
+ require "net/http"
2
+
3
+ module ItsyBtc
4
+ module Commands
5
+ class PushCommand < Command
6
+ name "push"
7
+ args "<path to tx>"
8
+ summary "push a transaction to the network"
9
+
10
+ def initialize(tx_path)
11
+ @tx_path = tx_path
12
+ end
13
+
14
+ def run
15
+ tx = Bitcoin::Protocol::Tx.from_json_file(@tx_path)
16
+ hex = tx.to_payload.unpack("H*")[0]
17
+
18
+ uri = URI("http://blockchain.info/pushtx")
19
+ response = Net::HTTP.post_form(uri, "tx" => hex)
20
+ if response.is_a? Net::HTTPSuccess
21
+ puts "Transaction pushed successfully."
22
+ else
23
+ puts "#{response.code} Error!"
24
+ puts "Blockchain.info says: #{response.body}" if response.response_body_permitted?
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+
@@ -0,0 +1,66 @@
1
+ module ItsyBtc
2
+ module Commands
3
+ class SignCommand < Command
4
+ name "sign"
5
+ args "<path to tx>"
6
+ summary "sign a transaction"
7
+
8
+ def initialize(tx_path)
9
+ @tx_path = tx_path
10
+ end
11
+
12
+ def run
13
+ if @wallet.nil?
14
+ puts "No wallet found! Use `itsy init` to create one."
15
+ exit
16
+ end
17
+
18
+ tx = Bitcoin::Protocol::Tx.from_json_file(@tx_path)
19
+
20
+ inputs_signed = 0
21
+ tx.in.each.with_index do |txin, i|
22
+ script = Bitcoin::Script.new(txin.script_sig)
23
+ if script.type == :unknown
24
+ puts "Input ##{i} already signed!"
25
+ inputs_signed += 1
26
+ else
27
+ address = case script.type
28
+ when :hash160
29
+ script.get_hash160_address
30
+ when :pubkey
31
+ script.get_pubkey_address
32
+ else
33
+ nil
34
+ end
35
+ if address
36
+ if base58_key = @wallet.key_for_address(address)
37
+ key = Bitcoin.open_key(Bitcoin::Key.from_base58(base58_key).priv)
38
+ sig = Bitcoin.sign_data(key, tx.signature_hash_for_input(i, nil, txin.script_sig))
39
+ txin.script_sig = Bitcoin::Script.to_signature_pubkey_script(sig, [key.public_key_hex].pack("H*"))
40
+ puts "Input ##{i} signed."
41
+ inputs_signed += 1
42
+ else
43
+ puts "Input ##{i} to address #{address} cannot be signed, that address is not in your wallet!"
44
+ end
45
+ else
46
+ puts "Input ##{i} cannot be signed, its script type is #{script.type} which is unsupported."
47
+ end
48
+ end
49
+ end
50
+
51
+ tx = Bitcoin::Protocol::Tx.new(tx.to_payload)
52
+
53
+ if inputs_signed == 0
54
+ puts "No inputs signed."
55
+ elsif inputs_signed == tx.in.length
56
+ File.open("#{tx.hash}-signed.json", "w") { |f| f << tx.to_json }
57
+ puts "Transaction signed, saved to #{tx.hash}-signed.json."
58
+ else
59
+ File.open("#{tx.hash}-signed#{inputs_signed}.json", "w") { |f| f << tx.to_json }
60
+ puts "#{inputs_signed} of #{tx.in.length} inputs signed, saved to #{tx.hash}-signed#{inputs_signed}.json."
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+
@@ -0,0 +1,152 @@
1
+ require 'bigdecimal'
2
+
3
+ module ItsyBtc
4
+ module Commands
5
+ class TxCommand < Command
6
+ name "tx"
7
+ args "[<amount> <recipient>]"
8
+ summary "create a transaction"
9
+
10
+ def initialize(amount = nil, recipient = nil)
11
+ @amount = amount
12
+ @recipient = recipient
13
+ end
14
+
15
+ def run
16
+ if @amount && @recipient
17
+ create_automatic_tx
18
+ else
19
+ create_custom_tx
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def create_automatic_tx
26
+ if @wallet.nil?
27
+ puts "No wallet found! Use `itsy init` to create one."
28
+ exit
29
+ end
30
+
31
+ if not Bitcoin.valid_address?(@recipient)
32
+ puts "The recipient address you specified is not a valid Bitcoin address."
33
+ exit
34
+ end
35
+
36
+ tx = Bitcoin::Protocol::Tx.new
37
+
38
+ amount = (BigDecimal.new(@amount) * 1e8).to_i
39
+ tx_fee = 10000
40
+ current_value = 0
41
+ @wallet.entries.shuffle.each do |e|
42
+ outputs = Lookup.unspent_outputs(e.address) || exit
43
+ outputs.shuffle.each do |output|
44
+ prev_tx_hash = [output[:tx_hash]].pack("H*").reverse.unpack("H*")[0]
45
+ prev_tx = Lookup.tx(prev_tx_hash) || exit
46
+ txin = Bitcoin::Protocol::TxIn.new(prev_tx.binary_hash, output[:tx_output_n], 0)
47
+ txin.script_sig = prev_tx.out[output[:tx_output_n]].pk_script
48
+ tx.add_in txin
49
+
50
+ current_value += output[:value]
51
+ break if current_value >= amount + tx_fee
52
+ end
53
+ break if current_value >= amount + tx_fee
54
+ end
55
+
56
+ if current_value < amount
57
+ puts "Insufficient funds! You only have #{Bitcoin.format_value(current_value)} BTC."
58
+ exit
59
+ elsif current_value < amount + tx_fee
60
+ tx_fee = current_value - amount
61
+ puts "Warning: only #{Bitcoin.format_value(tx_fee)} BTC left for transaction fees."
62
+ end
63
+
64
+ change = current_value - amount - tx_fee
65
+ change_address = @wallet.entries.sample.address
66
+
67
+ tx.add_out Bitcoin::Protocol::TxOut.value_to_address(amount, @recipient)
68
+ tx.add_out Bitcoin::Protocol::TxOut.value_to_address(change, change_address)
69
+
70
+ tx = Bitcoin::Protocol::Tx.new(tx.to_payload)
71
+
72
+ print "Will send #{ItsyBtc.format_value(amount)} BTC to #{@recipient}, "
73
+ if tx_fee == 0
74
+ print "with no transaction fees.\n"
75
+ else
76
+ print "with a fee of #{ItsyBtc.format_value(tx_fee)} BTC.\n"
77
+ end
78
+ if change > 0
79
+ puts "#{ItsyBtc.format_value(change)} BTC will be sent to #{change_address} as change."
80
+ end
81
+
82
+ File.open("#{tx.hash}.json", "w") { |f| f << tx.to_json }
83
+ puts "Transaction saved to #{tx.hash}.json."
84
+ end
85
+
86
+ def create_custom_tx
87
+ tx = Bitcoin::Protocol::Tx.new
88
+
89
+ total_input = 0
90
+ total_output = 0
91
+ loop do
92
+ msg = "(1) Add input, (2) Add output, (3) Show summary, (4) Save, or (5) Cancel? "
93
+ choice = ask(msg, Integer) { |q| q.in = 1..5 }
94
+ case choice
95
+ when 1
96
+ prev_tx = Lookup.tx(ask("Prev tx? ").to_s.strip) || exit
97
+ if prev_tx
98
+ output_index = ask("Output index? ", Integer)
99
+ txin = Bitcoin::Protocol::TxIn.new(prev_tx.binary_hash, output_index, 0)
100
+ txin.script_sig = prev_tx.out[output_index].pk_script
101
+ tx.add_in txin
102
+ puts "Input added. (#{ItsyBtc.format_value(prev_tx.out[output_index].value)} BTC)"
103
+ total_input += prev_tx.out[output_index].value
104
+ end
105
+ when 2
106
+ address = ask("Address? ").to_s
107
+ if Bitcoin.valid_address? address
108
+ amount = ask("Amount? ").to_s
109
+ value = BigDecimal.new(amount) * 1e8
110
+ raise "precision error! (TODO)" if value != value.to_i
111
+ tx.add_out Bitcoin::Protocol::TxOut.value_to_address(value.to_i, address)
112
+ puts "Output added. (#{ItsyBtc.format_value(value.to_i)} BTC)"
113
+ total_output += value.to_i
114
+ else
115
+ puts "Invalid address!"
116
+ end
117
+ when 3
118
+ print_summary(total_input, total_output, tx.out.length)
119
+ when 4
120
+ tx = Bitcoin::Protocol::Tx.new(tx.to_payload)
121
+ print_summary(total_input, total_output, tx.out.length)
122
+ File.open("#{tx.hash}.json", "w") { |f| f << tx.to_json }
123
+ puts "Transaction saved to #{tx.hash}.json."
124
+ exit
125
+ when 5
126
+ puts "No transaction created."
127
+ exit
128
+ end
129
+ end
130
+ end
131
+
132
+ def print_summary(total_input, total_output, num_addresses)
133
+ fees = total_input - total_output
134
+ print "Will send #{ItsyBtc.format_value(total_output)} BTC "
135
+ print "to #{num_addresses} address#{'es' if num_addresses != 1}, "
136
+ if fees == 0
137
+ print "with no transaction fees.\n"
138
+ else
139
+ print "with a fee of #{ItsyBtc.format_value(fees)} BTC.\n"
140
+ end
141
+ if fees < 0
142
+ puts "Warning! This transaction is spending more than it is receiving!"
143
+ elsif fees < 10000
144
+ puts "Warning! Transaction fees should be at least 0.0001 BTC!"
145
+ elsif fees > 1000000
146
+ puts "Warning! Transaction fees are very large."
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
152
+
@@ -0,0 +1,40 @@
1
+ require 'bigdecimal'
2
+
3
+ module ItsyBtc
4
+ module Commands
5
+ class VerifyCommand < Command
6
+ name "verify"
7
+ args "<path to tx>"
8
+ summary "verify that a transaction is signed"
9
+
10
+ def initialize(path)
11
+ @path = path
12
+ end
13
+
14
+ def run
15
+ tx = Bitcoin::Protocol::Tx.from_json_file(@path)
16
+
17
+ signed = true
18
+ tx.in.each.with_index do |txin, idx|
19
+ if Bitcoin::Script.new(txin.script_sig).type == :unknown
20
+ prev_tx = Lookup.tx(txin.previous_output)
21
+ if tx.verify_input_signature(idx, prev_tx)
22
+ puts "Input ##{idx} has a valid signature."
23
+ else
24
+ puts "Input ##{idx} has an invalid signature!"
25
+ signed = false
26
+ end
27
+ else
28
+ puts "Input ##{idx} is not signed."
29
+ signed = false
30
+ end
31
+ end
32
+
33
+ if tx.in.empty?
34
+ puts "Transaction has no inputs."
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+
@@ -0,0 +1,76 @@
1
+ module ItsyBtc
2
+ module Lookup
3
+ extend self
4
+
5
+ def tx(hash)
6
+ json_tx = open(ItsyBtc.blockexplorer_url + "/rawtx/#{hash}").read
7
+ Bitcoin::Protocol::Tx.from_json(json_tx)
8
+ rescue OpenURI::HTTPError => e
9
+ puts "Error looking up transaction: #{e.message}"
10
+ case e.io.status[0].to_i
11
+ when 400
12
+ puts "The transaction hash is invalid."
13
+ when 404
14
+ puts "blockexplorer.com doesn't know about the transaction."
15
+ else
16
+ puts "Make sure you have an internet connection and can load blockexplorer.com."
17
+ end
18
+ false
19
+ rescue SocketError => e
20
+ puts "Error looking up transaction: #{e.message}"
21
+ puts "Make sure you have an internet connection and can load blockexplorer.com."
22
+ false
23
+ end
24
+
25
+ def unspent_outputs(address)
26
+ json = open("http://blockchain.info/unspent?active=#{address}&format=json").read
27
+ data = JSON.parse(json, symbolize_names: true)
28
+ data[:unspent_outputs]
29
+ rescue OpenURI::HTTPError => e
30
+ if e.io.status[0].to_i == 500
31
+ []
32
+ else
33
+ puts "Error looking up unspent outputs: #{e.message}"
34
+ puts "Make sure you have an internet connection and can load blockchain.info."
35
+ false
36
+ end
37
+ rescue SocketEror => e
38
+ puts "Error looking up unspent outputs: #{e.message}"
39
+ puts "Make sure you have an internet connection and can load blockchain.info."
40
+ false
41
+ end
42
+
43
+ def balance(address)
44
+ html = open(ItsyBtc.blockexplorer_url + "/address/#{address}").read
45
+ received_btc = nil
46
+ sent_btc = nil
47
+ if html =~ /Received BTC: ([0-9.]+)/
48
+ received_btc = BigDecimal.new($1)
49
+ end
50
+ if html =~ /Sent BTC: ([0-9.]+)/
51
+ sent_btc = BigDecimal.new($1)
52
+ end
53
+ if received_btc && sent_btc
54
+ btc = received_btc - sent_btc
55
+ (btc * 1e8).to_i
56
+ else
57
+ puts "Can't parse the balance from blockexplorer.com's HTML. (They must have changed it.)"
58
+ false
59
+ end
60
+ rescue OpenURI::HTTPError => e
61
+ puts "Error looking up balance: #{e.message}"
62
+ case e.io.status[0].to_i
63
+ when 400
64
+ puts "The address is invalid."
65
+ else
66
+ puts "Make sure you have an internet connection and can load blockexplorer.com."
67
+ end
68
+ false
69
+ rescue SocketError => e
70
+ puts "Error looking up balance: #{e.message}"
71
+ puts "Make sure you have an internet connection and can load blockexplorer.com."
72
+ false
73
+ end
74
+ end
75
+ end
76
+
@@ -0,0 +1,156 @@
1
+ require "openssl"
2
+
3
+ module ItsyBtc
4
+ class Wallet
5
+ ALGORITHM = "aes-256-cbc"
6
+ PBKDF2_ITERATIONS = 4096
7
+ SALT_LEN = 8
8
+
9
+ class WalletError < RuntimeError; end
10
+ class WalletReadError < WalletError; end
11
+ class DuplicateAddressError < WalletError; end
12
+
13
+ attr_reader :path
14
+
15
+ def initialize(path)
16
+ @path = path
17
+ @passphrase = nil
18
+ end
19
+
20
+ def create(passphrase)
21
+ @passphrase = passphrase
22
+ write({entries: [], keys: encrypt_keys([])})
23
+ end
24
+
25
+ def add(base58, comment = "")
26
+ key = Bitcoin::Key.from_base58(base58)
27
+ data = read
28
+ raise DuplicateAddressError if data[:entries].any? { |e| e[:address] == key.addr }
29
+ data[:entries] << { address: key.addr, comment: comment }
30
+ keys = decrypt_keys(data[:keys])
31
+ keys << key.to_base58
32
+ data[:keys] = encrypt_keys(keys)
33
+ write(data)
34
+ end
35
+
36
+ def remove(address)
37
+ data = read
38
+ keys = decrypt_keys(data[:keys])
39
+
40
+ entry_index = data[:entries].index { |e| e[:address] == address }
41
+ if entry_index
42
+ data[:entries].delete_at(entry_index)
43
+ keys.delete_at(entry_index)
44
+
45
+ data[:keys] = encrypt_keys(keys)
46
+ write(data)
47
+ true
48
+ else
49
+ false
50
+ end
51
+ end
52
+
53
+ def verify_passphrase(passphrase)
54
+ @passphrase = passphrase
55
+ decrypt_keys(read[:keys])
56
+ nil
57
+ end
58
+
59
+ def change_passphrase(new_passphrase)
60
+ data = read
61
+ keys = decrypt_keys(data[:keys])
62
+ @passphrase = new_passphrase
63
+ data[:keys] = encrypt_keys(keys)
64
+ write(data)
65
+ end
66
+
67
+ def update_comment(address, comment)
68
+ data = read
69
+ entry_index = data[:entries].index { |e| e[:address] == address }
70
+ data[:entries][entry_index][:comment] = comment
71
+ write(data)
72
+ end
73
+
74
+ def addresses_starting_with(beginning)
75
+ entries.select { |e| e.address.start_with? beginning }
76
+ end
77
+
78
+ def key_for_address(address)
79
+ if entry = entries_with_keys.find { |e| e.address == address }
80
+ entry.key
81
+ end
82
+ end
83
+
84
+ def entries
85
+ read[:entries].inject([]) do |list, entry|
86
+ e = WalletEntry.new
87
+ e.address = entry[:address]
88
+ e.comment = entry[:comment]
89
+ list << e
90
+ end
91
+ end
92
+
93
+ def entries_with_keys
94
+ data = read
95
+ keys = decrypt_keys(data[:keys])
96
+ data[:entries].inject([]) do |list, entry|
97
+ e = WalletEntry.new
98
+ e.address = entry[:address]
99
+ e.comment = entry[:comment]
100
+ e.key = keys.shift
101
+ list << e
102
+ end
103
+ end
104
+
105
+ private
106
+
107
+ def read
108
+ data = JSON.parse(File.read(@path), symbolize_names: true)
109
+ raise "wallet is invalid!" if data[:entries].nil?
110
+ raise "wallet is invalid!" if data[:keys].nil?
111
+ data
112
+ end
113
+
114
+ def write(data)
115
+ raise "trying to write invalid data to wallet!" if data[:entries].nil?
116
+ raise "trying to write invalid data to wallet!" if data[:keys].nil?
117
+ File.write(@path, JSON.pretty_generate(data))
118
+ end
119
+
120
+ def encrypt_keys(keys)
121
+ encrypt(JSON.generate(keys)).unpack("H*")[0]
122
+ end
123
+
124
+ def decrypt_keys(hex)
125
+ JSON.parse(decrypt([hex].pack("H*")))
126
+ end
127
+
128
+ def encrypt(txt)
129
+ cipher = OpenSSL::Cipher.new(ALGORITHM)
130
+ cipher.encrypt
131
+ salt = OpenSSL::Random.random_bytes(SALT_LEN)
132
+ key_iv = OpenSSL::PKCS5.pbkdf2_hmac_sha1(passphrase, salt, PBKDF2_ITERATIONS, cipher.key_len + cipher.iv_len)
133
+ cipher.key = key_iv[0, cipher.key_len]
134
+ cipher.iv = key_iv[cipher.key_len, cipher.iv_len]
135
+ encrypted_txt = cipher.update(txt) + cipher.final
136
+
137
+ salt + encrypted_txt
138
+ end
139
+
140
+ def decrypt(data)
141
+ salt, encrypted_txt = data[0, SALT_LEN], data[SALT_LEN..-1]
142
+
143
+ cipher = OpenSSL::Cipher.new(ALGORITHM)
144
+ cipher.decrypt
145
+ key_iv = OpenSSL::PKCS5.pbkdf2_hmac_sha1(passphrase, salt, PBKDF2_ITERATIONS, cipher.key_len + cipher.iv_len)
146
+ cipher.key = key_iv[0, cipher.key_len]
147
+ cipher.iv = key_iv[cipher.key_len, cipher.iv_len]
148
+ cipher.update(encrypted_txt) + cipher.final
149
+ end
150
+
151
+ def passphrase
152
+ @passphrase ||= ask("Wallet passphrase? ") { |q| q.echo = false }
153
+ end
154
+ end
155
+ end
156
+
@@ -0,0 +1,6 @@
1
+ module ItsyBtc
2
+ class WalletEntry
3
+ attr_accessor :address, :key, :comment
4
+ end
5
+ end
6
+
metadata ADDED
@@ -0,0 +1,127 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: itsy-btc
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Jeremy Ruten
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-11-14 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: :runtime
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: bitcoin-ruby
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
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: ffi
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
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: highline
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: itsy-btc is a simple secure bitcoin wallet that allows you to turn an
70
+ offline computer into a hardware wallet.
71
+ email: jeremy.ruten@gmail.com
72
+ executables:
73
+ - itsy
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - Gemfile
78
+ - Gemfile.lock
79
+ - LICENSE
80
+ - itsy-btc.gemspec
81
+ - README.md
82
+ - lib/itsy-btc/commands/balance_command.rb
83
+ - lib/itsy-btc/commands/command.rb
84
+ - lib/itsy-btc/commands/comment_command.rb
85
+ - lib/itsy-btc/commands/decrypt_command.rb
86
+ - lib/itsy-btc/commands/delete_command.rb
87
+ - lib/itsy-btc/commands/gen_command.rb
88
+ - lib/itsy-btc/commands/help_command.rb
89
+ - lib/itsy-btc/commands/import_command.rb
90
+ - lib/itsy-btc/commands/init_command.rb
91
+ - lib/itsy-btc/commands/list_command.rb
92
+ - lib/itsy-btc/commands/passwd_command.rb
93
+ - lib/itsy-btc/commands/push_command.rb
94
+ - lib/itsy-btc/commands/sign_command.rb
95
+ - lib/itsy-btc/commands/tx_command.rb
96
+ - lib/itsy-btc/commands/verify_command.rb
97
+ - lib/itsy-btc/commands.rb
98
+ - lib/itsy-btc/lookup.rb
99
+ - lib/itsy-btc/wallet.rb
100
+ - lib/itsy-btc/wallet_entry.rb
101
+ - lib/itsy-btc.rb
102
+ - bin/itsy
103
+ homepage: http://github.com/yjerem/itsy-btc
104
+ licenses:
105
+ - MIT
106
+ metadata: {}
107
+ post_install_message:
108
+ rdoc_options: []
109
+ require_paths:
110
+ - lib
111
+ required_ruby_version: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - '>='
114
+ - !ruby/object:Gem::Version
115
+ version: 1.9.2
116
+ required_rubygems_version: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - '>='
119
+ - !ruby/object:Gem::Version
120
+ version: '0'
121
+ requirements: []
122
+ rubyforge_project:
123
+ rubygems_version: 2.0.3
124
+ signing_key:
125
+ specification_version: 4
126
+ summary: A simple secure bitcoin wallet.
127
+ test_files: []