package blob import ( "bytes" "encoding/binary" "errors" "fmt" "io" "golang.org/x/crypto/chacha20poly1305" "github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/txscript" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwire" ) const ( // MinVersion is the minimum blob version supported by this package. MinVersion = 0 // MaxVersion is the maximumm blob version supported by this package. MaxVersion = 0 // NonceSize is the length of a chacha20poly1305 nonce, 12 bytes. NonceSize = chacha20poly1305.NonceSize // KeySize is the length of a chacha20poly1305 key, 32 bytes. KeySize = chacha20poly1305.KeySize // CiphertextExpansion is the number of bytes padded to a plaintext // encrypted with chacha20poly1305, which comes from a 16-byte MAC. CiphertextExpansion = 16 // V0PlaintextSize is the plaintext size of a version 0 encoded blob. // sweep address: 42 bytes // revocation pubkey: 33 bytes // local delay pubkey: 33 bytes // csv delay: 4 bytes // commit to-local revocation sig: 64 bytes // commit to-remote pubkey: 33 bytes, maybe blank // commit to-remote sig: 64 bytes, maybe blank V0PlaintextSize = 273 ) // Size returns the size of the encoded-and-encrypted blob in bytes. // enciphered plaintext: n bytes // MAC: 16 bytes func Size(ver uint16) int { return PlaintextSize(ver) + CiphertextExpansion } // PlaintextSize returns the size of the encoded-but-unencrypted blob in bytes. func PlaintextSize(ver uint16) int { switch ver { case 0: return V0PlaintextSize default: return 0 } } var ( // byteOrder specifies a big-endian encoding of all integer values. byteOrder = binary.BigEndian // ErrUnknownBlobVersion signals that we don't understand the requested // blob encoding scheme. ErrUnknownBlobVersion = errors.New("unknown blob version") // ErrCiphertextTooSmall is a decryption error signaling that the // ciphertext is smaller than the ciphertext expansion factor. ErrCiphertextTooSmall = errors.New( "ciphertext is too small for chacha20poly1305", ) // ErrNonceSize signals that the provided nonce is improperly sized. ErrNonceSize = fmt.Errorf( "chacha20poly1305 nonce must be %d bytes", NonceSize, ) // ErrKeySize signals that the provided key is improperly sized. ErrKeySize = fmt.Errorf( "chacha20poly1305 key size must be %d bytes", KeySize, ) // ErrNoCommitToRemoteOutput is returned when trying to retrieve the // commit to-remote output from the blob, though none exists. ErrNoCommitToRemoteOutput = errors.New( "cannot obtain commit to-remote p2wkh output script from blob", ) ) // PubKey is a 33-byte, serialized compressed public key. type PubKey [33]byte // JusticeKit is lé Blob of Justice. The JusticeKit contains information // required to construct a justice transaction, that sweeps a remote party's // revoked commitment transaction. It supports encryption and decryption using // chacha20poly1305, allowing the client to encrypt the contents of the blob, // and for a watchtower to later decrypt if action must be taken. The encoding // format is versioned to allow future extensions. type JusticeKit struct { // SweepAddress is the witness program of the output where the client's // fund will be deposited. This value is included in the blobs, as // opposed to the session info, such that the sweep addresses can't be // correlated across sessions and/or towers. // // NOTE: This is chosen to be the length of a maximally sized witness // program. SweepAddress [42]byte // RevocationPubKey is the compressed pubkey that guards the revocation // clause of the remote party's to-local output. RevocationPubKey PubKey // LocalDelayPubKey is the compressed pubkey in the to-local script of // the remote party, which guards the path where the remote party // claims their commitment output. LocalDelayPubKey PubKey // CSVDelay is the relative timelock in the remote party's to-local // output, which the remote party must wait out before sweeping their // commitment output. CSVDelay uint32 // CommitToLocalSig is a signature under RevocationPubKey using // SIGHASH_ALL. CommitToLocalSig lnwire.Sig // CommitToRemotePubKey is the public key in the to-remote output of the revoked // commitment transaction. // // NOTE: This value is only used if it contains a valid compressed // public key. CommitToRemotePubKey PubKey // CommitToRemoteSig is a signature under CommitToRemotePubKey using SIGHASH_ALL. // // NOTE: This value is only used if CommitToRemotePubKey contains a valid // compressed public key. CommitToRemoteSig lnwire.Sig } // CommitToLocalWitnessScript returns the serialized witness script for the // commitment to-local output. func (b *JusticeKit) CommitToLocalWitnessScript() ([]byte, error) { revocationPubKey, err := btcec.ParsePubKey( b.RevocationPubKey[:], btcec.S256(), ) if err != nil { return nil, err } localDelayedPubKey, err := btcec.ParsePubKey( b.LocalDelayPubKey[:], btcec.S256(), ) if err != nil { return nil, err } return lnwallet.CommitScriptToSelf( b.CSVDelay, localDelayedPubKey, revocationPubKey, ) } // CommitToLocalRevokeWitnessStack constructs a witness stack spending the // revocation clause of the commitment to-local output. // 1 func (b *JusticeKit) CommitToLocalRevokeWitnessStack() [][]byte { witnessStack := make([][]byte, 2) witnessStack[0] = append(b.CommitToLocalSig[:], byte(txscript.SigHashAll)) witnessStack[1] = []byte{1} return witnessStack } // HasCommitToRemoteOutput returns true if the blob contains a to-remote p2wkh // pubkey. func (b *JusticeKit) HasCommitToRemoteOutput() bool { return btcec.IsCompressedPubKey(b.CommitToRemotePubKey[:]) } // CommitToRemoteWitnessScript returns the witness script for the commitment // to-remote p2wkh output, which is the pubkey itself. func (b *JusticeKit) CommitToRemoteWitnessScript() ([]byte, error) { if !btcec.IsCompressedPubKey(b.CommitToRemotePubKey[:]) { return nil, ErrNoCommitToRemoteOutput } return b.CommitToRemotePubKey[:], nil } // CommitToRemoteWitnessStack returns a witness stack spending the commitment // to-remote output, which is a regular p2wkh. // func (b *JusticeKit) CommitToRemoteWitnessStack() [][]byte { witnessStack := make([][]byte, 1) witnessStack[0] = append(b.CommitToRemoteSig[:], byte(txscript.SigHashAll)) return witnessStack } // Encrypt encodes the blob of justice using encoding version, and then // creates a ciphertext using chacha20poly1305 under the chosen (nonce, key) // pair. // // NOTE: It is the caller's responsibility to ensure that this method is only // called once for a given (nonce, key) pair. func (b *JusticeKit) Encrypt(nonce, key []byte, version uint16) ([]byte, error) { switch { // Fail if the nonce is not 12-bytes. case len(nonce) != NonceSize: return nil, ErrNonceSize // Fail if the nonce is not 32-bytes. case len(key) != KeySize: return nil, ErrKeySize } // Encode the plaintext using the provided version, to obtain the // plaintext bytes. var ptxtBuf bytes.Buffer err := b.encode(&ptxtBuf, version) if err != nil { return nil, err } // Create a new chacha20poly1305 cipher, using a 32-byte key. cipher, err := chacha20poly1305.New(key) if err != nil { return nil, err } // Allocate the ciphertext, which will contain the encrypted plaintext // and MAC. plaintext := ptxtBuf.Bytes() ciphertext := make([]byte, len(plaintext)+CiphertextExpansion) // Finally, encrypt the plaintext using the given nonce, storing the // result in the ciphertext buffer. cipher.Seal(ciphertext[:0], nonce, plaintext, nil) return ciphertext, nil } // Decrypt unenciphers a blob of justice by decrypting the ciphertext using // chacha20poly1305 with the chosen (nonce, key) pair. The internal plaintext is // then deserialized using the given encoding version. func Decrypt(nonce, key, ciphertext []byte, version uint16) (*JusticeKit, error) { switch { // Fail if the blob's overall length is less than the expansion factor. case len(ciphertext) < CiphertextExpansion: return nil, ErrCiphertextTooSmall // Fail if the nonce is not 12-bytes. case len(nonce) != NonceSize: return nil, ErrNonceSize // Fail if the key is not 32-bytes. case len(key) != KeySize: return nil, ErrKeySize } // Create a new chacha20poly1305 cipher, using a 32-byte key. cipher, err := chacha20poly1305.New(key) if err != nil { return nil, err } // Allocate the final buffer that will contain the blob's plaintext // bytes, which is computed by subtracting the ciphertext expansion // factor from the blob's length. plaintext := make([]byte, len(ciphertext)-CiphertextExpansion) // Decrypt the ciphertext, placing the resulting plaintext in our // plaintext buffer. _, err = cipher.Open(plaintext[:0], nonce, ciphertext, nil) if err != nil { return nil, err } // If decryption succeeded, we will then decode the plaintext bytes // using the specified blob version. boj := &JusticeKit{} err = boj.decode(bytes.NewReader(plaintext), version) if err != nil { return nil, err } return boj, nil } // encode serializes the JusticeKit according to the version, returning an // error if the version is unknown. func (b *JusticeKit) encode(w io.Writer, ver uint16) error { switch ver { case 0: return b.encodeV0(w) default: return ErrUnknownBlobVersion } } // decode deserializes the JusticeKit according to the version, returning an // error if the version is unknown. func (b *JusticeKit) decode(r io.Reader, ver uint16) error { switch ver { case 0: return b.decodeV0(r) default: return ErrUnknownBlobVersion } } // encodeV0 encodes the JusticeKit using the version 0 encoding scheme to the // provided io.Writer. The encoding supports sweeping of the commit to-local // output, and optionally the commit to-remote output. The encoding produces a // constant-size plaintext size of 273 bytes. // // blob version 0 plaintext encoding: // sweep address: 42 bytes // revocation pubkey: 33 bytes // local delay pubkey: 33 bytes // csv delay: 4 bytes // commit to-local revocation sig: 64 bytes // commit to-remote pubkey: 33 bytes, maybe blank // commit to-remote sig: 64 bytes, maybe blank func (b *JusticeKit) encodeV0(w io.Writer) error { // Write 42-byte sweep address where client funds will be deposited. _, err := w.Write(b.SweepAddress[:]) if err != nil { return err } // Write 33-byte revocation public key. _, err = w.Write(b.RevocationPubKey[:]) if err != nil { return err } // Write 33-byte local delay public key. _, err = w.Write(b.LocalDelayPubKey[:]) if err != nil { return err } // Write 4-byte CSV delay. err = binary.Write(w, byteOrder, b.CSVDelay) if err != nil { return err } // Write 64-byte revocation signature for commit to-local output. _, err = w.Write(b.CommitToLocalSig[:]) if err != nil { return err } // Write 33-byte commit to-remote public key, which may be blank. _, err = w.Write(b.CommitToRemotePubKey[:]) if err != nil { return err } // Write 64-byte commit to-remote signature, which may be blank. _, err = w.Write(b.CommitToRemoteSig[:]) return err } // decodeV0 reconstructs a JusticeKit from the io.Reader, using version 0 // encoding scheme. This will parse a constant size input stream of 273 bytes to // recover information for the commit to-local output, and possibly the commit // to-remote output. // // blob version 0 plaintext encoding: // sweep address: 42 bytes // revocation pubkey: 33 bytes // local delay pubkey: 33 bytes // csv delay: 4 bytes // commit to-local revocation sig: 64 bytes // commit to-remote pubkey: 33 bytes, maybe blank // commit to-remote sig: 64 bytes, maybe blank func (b *JusticeKit) decodeV0(r io.Reader) error { // Read 42-byte sweep address where client funds will be deposited. _, err := io.ReadFull(r, b.SweepAddress[:]) if err != nil { return err } // Read 33-byte revocation public key. _, err = io.ReadFull(r, b.RevocationPubKey[:]) if err != nil { return err } // Read 33-byte local delay public key. _, err = io.ReadFull(r, b.LocalDelayPubKey[:]) if err != nil { return err } // Read 4-byte CSV delay. err = binary.Read(r, byteOrder, &b.CSVDelay) if err != nil { return err } // Read 64-byte revocation signature for commit to-local output. _, err = io.ReadFull(r, b.CommitToLocalSig[:]) if err != nil { return err } var ( commitToRemotePubkey PubKey commitToRemoteSig lnwire.Sig ) // Read 33-byte commit to-remote public key, which may be discarded. _, err = io.ReadFull(r, commitToRemotePubkey[:]) if err != nil { return err } // Read 64-byte commit to-remote signature, which may be discarded. _, err = io.ReadFull(r, commitToRemoteSig[:]) if err != nil { return err } // Only populate the commit to-remote fields in the decoded blob if a // valid compressed public key was read from the reader. if btcec.IsCompressedPubKey(commitToRemotePubkey[:]) { b.CommitToRemotePubKey = commitToRemotePubkey b.CommitToRemoteSig = commitToRemoteSig } return nil }