Browse Source

Add atto-safesign

xno
codesoap 9 months ago
parent
commit
01d6818323
  1. 3
      README.md
  2. 61
      cmd/atto-safesign/README.md
  3. 13
      cmd/atto-safesign/config.go
  4. 286
      cmd/atto-safesign/main.go
  5. 137
      cmd/atto-safesign/util.go

3
README.md

@ -109,6 +109,9 @@ file system. This makes atto very portable, but also means, that @@ -109,6 +109,9 @@ file system. This makes atto very portable, but also means, that
no history is stored locally. I recommend using a service like
https://nanocrawler.cc/ to investigate transaction history.
# Offline signing
See [atto-safesign](cmd/atto-safesign/).
# Donations
If you want to show your appreciation for atto, you can donate to me at
`nano_1i7wsbehgwhxct91wpojr1j588ydikd64uc7p3kj54nofqioc6ydjopezf13`.

61
cmd/atto-safesign/README.md

@ -0,0 +1,61 @@ @@ -0,0 +1,61 @@
# Creating blocks from an offline computer
If you want to keep your seed extra safe you may choose to never take
it onto a computer that is connected to the internet. `atto-safesign`
enables you to do this by creating a file which contains initially
unsigned blocks. The blocks in this file can then be signed on the
offline computer and transferred back to the online computer to submit
the blocks to the Nano network.
To transfer data between the off- and online computer, you can use a
USB thumb drive, but make sure that there is no maleware on the drive.
A safer alternative could be to use QR codes (e.g. with the tools
`qrencode` and `zbarimg`), to transfer the atto file with the blocks to
and from the offline computer after manually checking the contents of
the file.
# Usage
```
online$ # These steps take place on an online computer:
online$ echo $MY_PUBLIC_KEY | atto-safesign test.atto receive
online$ echo $MY_PUBLIC_KEY | atto-safesign test.atto representative nano_3up3y8cd3hhs7zdpmkpssgb1iyjpke3xwmgqy8rg58z1hwryqpjqnkuqayps
offline$ # The sign subcommand can then be used on an offline computer:
offline$ pass nano | atto-safesign test.atto sign
online$ # Back at the online computer, the now signed blocks can be submitted:
online$ atto-safesign test.atto submit
```
```
$ atto-safesign -h
Usage:
atto-safesign -v
atto-safesign FILE receive
atto-safesign FILE representative REPRESENTATIVE
atto-safesign FILE send AMOUNT RECEIVER
atto-safesign [-a ACCOUNT_INDEX] [-y] FILE sign
atto-safesign FILE submit
If the -v flag is provided, atto-safesign will print its version number.
The receive, representative, send and submit subcommands expect a Nano
address as the first line of their standard input.
The sign subcommand expects a seed as the first line of standard input.
It also expects manual confirmation before signing blocks, unless the -y
flag is given.
The receive, representative and send subcommands will generate blocks
and append them to FILE. The blocks will still be lacking their
signature.
The sign subcommand will add signatures to all blocks in FILE. It is the
only subcommand that requires no network connection.
The submit subcommand will submit all blocks contained in FILE to the
Nano network.
ACCOUNT_INDEX is an optional parameter, which allows you to use
different accounts derived from the given seed. By default the account
with index 0 is chosen.
```

13
cmd/atto-safesign/config.go

@ -0,0 +1,13 @@ @@ -0,0 +1,13 @@
package main
var (
// The node needs to support the work_generate action.
// See e.g. https://publicnodes.somenano.com to find public nodes or
// set up your own node to use. Here are some public alternatives:
// - https://proxy.nanos.cc/proxy
// - https://mynano.ninja/api/node
// - https://rainstorm.city/api
node = "https://proxy.powernode.cc/proxy"
defaultRepresentative = "nano_18shbirtzhmkf7166h39nowj9c9zrpufeg75bkbyoobqwf1iu3srfm9eo3pz"
)

286
cmd/atto-safesign/main.go

@ -0,0 +1,286 @@ @@ -0,0 +1,286 @@
package main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"io"
"math/big"
"os"
"github.com/codesoap/atto"
)
var usage = `Usage:
atto-safesign -v
atto-safesign FILE receive
atto-safesign FILE representative REPRESENTATIVE
atto-safesign FILE send AMOUNT RECEIVER
atto-safesign [-a ACCOUNT_INDEX] [-y] FILE sign
atto-safesign FILE submit
If the -v flag is provided, atto-safesign will print its version number.
The receive, representative, send and submit subcommands expect a Nano
address as the first line of their standard input.
The sign subcommand expects a seed as the first line of standard input.
It also expects manual confirmation before signing blocks, unless the -y
flag is given.
The receive, representative and send subcommands will generate blocks
and append them to FILE. The blocks will still be lacking their
signature.
The sign subcommand will add signatures to all blocks in FILE. It is the
only subcommand that requires no network connection.
The submit subcommand will submit all blocks contained in FILE to the
Nano network.
ACCOUNT_INDEX is an optional parameter, which allows you to use
different accounts derived from the given seed. By default the account
with index 0 is chosen.
`
var accountIndexFlag uint
var yFlag bool
func init() {
var vFlag bool
flag.Usage = func() { fmt.Fprint(os.Stderr, usage) }
flag.UintVar(&accountIndexFlag, "a", 0, "")
flag.BoolVar(&yFlag, "y", false, "")
flag.BoolVar(&vFlag, "v", false, "")
flag.Parse()
if vFlag {
fmt.Println("1.0.0")
os.Exit(0)
}
if flag.NArg() < 2 {
flag.Usage()
os.Exit(1)
}
var ok bool
switch flag.Arg(1) {
case "receive", "sign", "submit":
ok = flag.NArg() == 2
case "representative":
ok = flag.NArg() == 3
case "send":
ok = flag.NArg() == 4
}
if !ok {
flag.Usage()
os.Exit(1)
}
}
func main() {
var err error
switch flag.Arg(1) {
case "receive":
err = receive()
case "representative":
err = change()
case "send":
err = send()
case "sign":
err = sign()
case "submit":
err = submit()
}
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(2)
}
}
func receive() error {
addr, err := getFirstStdinLine()
if err != nil {
return err
}
account, err := atto.NewAccountFromAddress(addr)
if err != nil {
return err
}
info, err := getLatestAccountInfo(account)
if err != nil {
return err
}
pendings, err := account.FetchPending(node)
if err != nil {
return err
}
for _, pending := range pendings {
block, err := info.Receive(pending)
if err != nil {
return err
}
if err = block.FetchWork(node); err != nil {
return err
}
blockJSON, err := json.Marshal(block)
if err != nil {
return err
}
err = appendLineToFile(blockJSON)
if err != nil {
return err
}
}
return nil
}
func change() error {
representative := flag.Arg(2)
addr, err := getFirstStdinLine()
if err != nil {
return err
}
account, err := atto.NewAccountFromAddress(addr)
if err != nil {
return err
}
info, err := getLatestAccountInfo(account)
if err != nil {
return err
}
block, err := info.Change(representative)
if err != nil {
return err
}
if err = block.FetchWork(node); err != nil {
return err
}
blockJSON, err := json.Marshal(block)
if err != nil {
return err
}
return appendLineToFile(blockJSON)
}
func send() error {
amount := flag.Arg(2)
receiver := flag.Arg(3)
addr, err := getFirstStdinLine()
if err != nil {
return err
}
account, err := atto.NewAccountFromAddress(addr)
if err != nil {
return err
}
info, err := getLatestAccountInfo(account)
if err != nil {
return err
}
block, err := info.Send(amount, receiver)
if err != nil {
return err
}
if err = block.FetchWork(node); err != nil {
return err
}
blockJSON, err := json.Marshal(block)
if err != nil {
return err
}
return appendLineToFile(blockJSON)
}
func sign() error {
seed, err := getFirstStdinLine()
if err != nil {
return err
}
privateKey, err := atto.NewPrivateKey(seed, uint32(accountIndexFlag))
if err != nil {
return err
}
account, err := atto.NewAccount(privateKey)
if err != nil {
return err
}
blocks, err := getBlocksFromFile()
if err != nil {
return err
}
var outBuffer bytes.Buffer
for _, block := range blocks {
if account.Address != block.Account {
txt := "Used account with address '%s' cannot sign block with address '%s'"
return fmt.Errorf(txt, account.Address, block.Account)
}
if err = letUserVerifyBlock(block); err != nil {
return err
}
block.Sign(privateKey)
blockJSON, err := json.Marshal(block)
if err != nil {
return err
}
// Buffer output so that file can be overwritten as late as possible
// to avoid problems during the write as much as possible.
outBuffer.Write(blockJSON) // err is always nil.
outBuffer.Write([]byte{'\n'}) // err is always nil.
}
file, err := os.Create(flag.Arg(0))
if err != nil {
return err
}
defer file.Close()
_, err = io.Copy(file, &outBuffer)
return err
}
func submit() error {
addr, err := getFirstStdinLine()
if err != nil {
return err
}
account, err := atto.NewAccountFromAddress(addr)
if err != nil {
return err
}
blocks, err := getBlocksFromFile()
if err != nil {
return err
}
info, err := account.FetchAccountInfo(node)
if err != nil {
return err
}
oldBalance, ok := big.NewInt(0).SetString(info.Balance, 10)
if !ok {
fmt.Errorf("cannot parse '%s' as an integer", info.Balance)
}
for _, block := range blocks {
newBalance, ok := big.NewInt(0).SetString(block.Balance, 10)
if !ok {
fmt.Errorf("cannot parse '%s' as an integer", block.Balance)
}
switch oldBalance.Cmp(newBalance) {
case -1:
block.SubType = atto.SubTypeReceive
case 0:
// If the balance does not change, this should be a "change" block.
block.SubType = atto.SubTypeChange
case 1:
block.SubType = atto.SubTypeSend
}
fmt.Fprint(os.Stderr, "Submitting block... ")
err = block.Submit(node)
if err != nil {
return err
}
fmt.Fprintln(os.Stderr, "done")
oldBalance = newBalance
}
return nil
}

137
cmd/atto-safesign/util.go

@ -0,0 +1,137 @@ @@ -0,0 +1,137 @@
package main
import (
"bufio"
"encoding/json"
"flag"
"fmt"
"io"
"math/big"
"os"
"runtime"
"strings"
"github.com/codesoap/atto"
)
func getFirstStdinLine() (string, error) {
in := bufio.NewReader(os.Stdin)
firstLine, err := in.ReadString('\n')
if err != nil {
return "", err
}
return strings.TrimSpace(firstLine), nil
}
// getLatestAccountInfo returns an atto.AccountInfo with the latest
// available block as it's Frontier. This is either the last block from
// the file or the one fetched from the network, if the file contains no
// blocks.
func getLatestAccountInfo(acc atto.Account) (atto.AccountInfo, error) {
blocks, err := getBlocksFromFile()
if err != nil {
return atto.AccountInfo{}, err
}
if len(blocks) == 0 {
return acc.FetchAccountInfo(node)
}
latestBlock := blocks[len(blocks)-1]
hash, err := latestBlock.Hash()
if err != nil {
return atto.AccountInfo{}, err
}
info := atto.AccountInfo{
Frontier: hash,
Representative: latestBlock.Representative,
Balance: latestBlock.Balance,
PublicKey: acc.PublicKey,
Address: acc.Address,
}
return info, nil
}
func getBlocksFromFile() ([]atto.Block, error) {
file, err := os.Open(flag.Arg(0))
if err != nil {
// The file has not been found, which is OK.
return []atto.Block{}, nil
}
defer file.Close()
reader := bufio.NewReader(file)
blocks := make([]atto.Block, 0)
for {
line, err := reader.ReadBytes('\n')
if err == io.EOF {
break
} else if err != nil {
return nil, err
}
if len(line) < 2 || line[0] == '#' {
// Skip empty and comment lines
continue
}
var block atto.Block
if err = json.Unmarshal(line, &block); err != nil {
return nil, err
}
blocks = append(blocks, block)
}
return blocks, nil
}
func appendLineToFile(in []byte) error {
file, err := os.OpenFile(flag.Arg(0), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer file.Close()
if _, err = file.Write(in); err != nil {
return err
}
_, err = file.Write([]byte{'\n'})
return err
}
func rawToNanoString(raw *big.Int) string {
rawPerKnano, _ := big.NewInt(0).SetString("1000000000000000000000000000", 10)
balance := big.NewInt(0).Div(raw, rawPerKnano).Int64()
if balance < 0 {
balance = -balance
return fmt.Sprintf("-%d.%03d NANO", balance/1000, balance%1000)
}
return fmt.Sprintf("%d.%03d NANO", balance/1000, balance%1000)
}
func letUserVerifyBlock(block atto.Block) (err error) {
if !yFlag {
balanceInt, ok := big.NewInt(0).SetString(block.Balance, 10)
if !ok {
return fmt.Errorf("cannot parse '%s' as an integer", block.Balance)
}
balanceNano := rawToNanoString(balanceInt)
txt := "Sign block that sets balance to %s and representative to %s? [y/N]: "
fmt.Fprintf(os.Stderr, txt, balanceNano, block.Representative)
// Explicitly openning /dev/tty or CONIN$ ensures function, even if
// the standard input is not a terminal.
var tty *os.File
if runtime.GOOS == "windows" {
tty, err = os.Open("CONIN$")
} else {
tty, err = os.Open("/dev/tty")
}
if err != nil {
msg := "could not open terminal for confirmation input: %v"
return fmt.Errorf(msg, err)
}
defer tty.Close()
var confirmation string
fmt.Fscanln(tty, &confirmation)
if confirmation != "y" && confirmation != "Y" {
fmt.Fprintln(os.Stderr, "Signing aborted.")
os.Exit(0)
}
}
return
}
Loading…
Cancel
Save