Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend the capability of decode RPC to decrypt the contents of emergency.recover file. #6773

Merged
merged 4 commits into from
Oct 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .msggen.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"bolt12 invoice": 1,
"bolt12 invoice_request": 2,
"bolt12 offer": 0,
"emergency recover": 5,
"rune": 4
},
"DecodepayFallbacksType": {
Expand Down Expand Up @@ -484,6 +485,7 @@
"DecodeResponse": {
"Decode.created_at": 60,
"Decode.currency_minor_unit": 8,
"Decode.decrypted": 76,
"Decode.description_hash": 64,
"Decode.expiry": 61,
"Decode.extra[]": 69,
Expand Down Expand Up @@ -1973,6 +1975,10 @@
"added": "pre-v0.10.1",
"deprecated": false
},
"Decode.decrypted": {
"added": "v23.11",
"deprecated": false
},
"Decode.description_hash": {
"added": "pre-v0.10.1",
"deprecated": false
Expand Down
2 changes: 2 additions & 0 deletions cln-grpc/proto/node.proto

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions cln-grpc/src/convert.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions cln-rpc/src/model.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

248 changes: 124 additions & 124 deletions contrib/pyln-grpc-proto/pyln/grpc/node_pb2.py

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions contrib/pyln-testing/pyln/testing/grpc2py.py
Original file line number Diff line number Diff line change
Expand Up @@ -1186,6 +1186,7 @@ def decode2py(m):
"restrictions": [decode_restrictions2py(i) for i in m.restrictions], # ArrayField[composite] in generate_composite
"warning_rune_invalid_utf8": m.warning_rune_invalid_utf8, # PrimitiveField in generate_composite
"hex": hexlify(m.hex), # PrimitiveField in generate_composite
"decrypted": hexlify(m.decrypted), # PrimitiveField in generate_composite
})


Expand Down
9 changes: 7 additions & 2 deletions doc/lightning-decode.7.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ The **decode** RPC command checks and parses:
or `LIGHTNING:`) as specified by the BOLT 11 and BOLT 12
specifications.
- a *rune* as created by lightning-commando-rune(7).
- a *emergency\_recover* starting with clnemerg1 which contains the content of emergency.recover file.

It may decode other formats in future.

Expand All @@ -24,7 +25,7 @@ RETURN VALUE
[comment]: # (GENERATE-FROM-SCHEMA-START)
On success, an object is returned, containing:

- **type** (string): what kind of object it decoded to (one of "bolt12 offer", "bolt12 invoice", "bolt12 invoice\_request", "bolt11 invoice", "rune")
- **type** (string): what kind of object it decoded to (one of "bolt12 offer", "bolt12 invoice", "bolt12 invoice\_request", "bolt11 invoice", "rune", "emergency recover")
- **valid** (boolean): if this is false, you *MUST* not use the result except for diagnostics!

If **type** is "bolt12 offer", and **valid** is *true*:
Expand Down Expand Up @@ -281,6 +282,10 @@ If **type** is "rune", and **valid** is *false*:
- the following warnings are possible:
- **warning\_rune\_invalid\_utf8**: the rune contains invalid UTF-8 strings

If **type** is "emergency recover", and **valid** is *true*:

- **decrypted** (hex): The decrypted value of the provided bech32 of emergency.recover *(added v23.11)*

[comment]: # (GENERATE-FROM-SCHEMA-END)

AUTHOR
Expand All @@ -303,4 +308,4 @@ RESOURCES

Main web site: <https://github.com/ElementsProject/lightning>

[comment]: # ( SHA256STAMP:ee1667eb4bf5eda980615dbcee334f618991e4b648a07ded8868ecde09bb2554)
[comment]: # ( SHA256STAMP:d62327dbe56d27e5e82d5ad2599d3d88495cc8360d84ff02fca59d08ab7fa14e)
36 changes: 35 additions & 1 deletion doc/schemas/decode.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"bolt12 invoice",
"bolt12 invoice_request",
"bolt11 invoice",
"rune"
"rune",
"emergency recover"
],
"description": "what kind of object it decoded to"
},
Expand Down Expand Up @@ -1509,6 +1510,39 @@
}
}
}
},
{
"if": {
"properties": {
"type": {
"type": "string",
"enum": [
"emergency recover"
]
},
"valid": {
"type": "boolean",
"enum": [
true
]
}
}
},
"then": {
"required": [
"decrypted"
],
"additionalProperties": false,
"properties": {
"type": {},
"valid": {},
"decrypted": {
"type": "hex",
"description": "The decrypted value of the provided bech32 of emergency.recover",
"added": "v23.11"
}
}
}
}
]
}
109 changes: 109 additions & 0 deletions plugins/offers.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <ccan/rune/rune.h>
#include <ccan/tal/str/str.h>
#include <common/bech32.h>
#include <common/bech32_util.h>
#include <common/bolt11.h>
#include <common/bolt11_json.h>
#include <common/bolt12_merkle.h>
Expand All @@ -17,6 +18,10 @@
#include <plugins/offers_inv_hook.h>
#include <plugins/offers_invreq_hook.h>
#include <plugins/offers_offer.h>
#include <sodium.h>

#define HEADER_LEN crypto_secretstream_xchacha20poly1305_HEADERBYTES
#define ABYTES crypto_secretstream_xchacha20poly1305_ABYTES

struct pubkey id;
u32 blockheight;
Expand Down Expand Up @@ -159,8 +164,41 @@ struct decodable {
struct tlv_invoice *invoice;
struct tlv_invoice_request *invreq;
struct rune *rune;
u8 *emergency_recover;
};

static u8 *encrypted_decode(const tal_t *ctx, const char *str, char **fail) {
if (strlen(str) < 8) {
*fail = tal_fmt(ctx, "invalid payload");
return NULL;
}

size_t hrp_maxlen = strlen(str) - 6;
char *hrp = tal_arr(ctx, char, hrp_maxlen);

size_t data_maxlen = strlen(str) - 8;
u5 *data = tal_arr(ctx, u5, data_maxlen);
size_t datalen = 0;

if (bech32_decode(hrp, data, &datalen, str, (size_t)-1)
== BECH32_ENCODING_NONE) {
*fail = tal_fmt(ctx, "invalid bech32 encoding");
goto fail;
}

if (!streq(hrp, "clnemerg")) {
*fail = tal_fmt(ctx, "hrp should be `clnemerg`");
goto fail;
}
u8 *data8bit = tal_arr(data, u8, 0);
bech32_pull_bits(&data8bit, data, datalen*5);

return data8bit;
fail:
tal_free(data);
return NULL;
}

static struct command_result *param_decodable(struct command *cmd,
const char *name,
const char *buffer,
Expand Down Expand Up @@ -214,6 +252,17 @@ static struct command_result *param_decodable(struct command *cmd,
return NULL;
}

decodable->emergency_recover = encrypted_decode(cmd, tal_strndup(tmpctx, buffer + tok.start,
tok.end - tok.start),
json_tok_startswith(buffer, &tok,
"clnemerg1")
? &likely_fail : &fail);

if (decodable->emergency_recover) {
decodable->type = "emergency recover";
return NULL;
}

/* If no other was likely, bolt11 decoder gives us failure string. */
decodable->b11 = bolt11_decode(cmd,
tal_strndup(tmpctx, buffer + tok.start,
Expand Down Expand Up @@ -1009,6 +1058,55 @@ static void json_add_rune(struct command *cmd, struct json_stream *js, const str
json_add_bool(js, "valid", true);
}

static struct command_result *after_makesecret(struct command *cmd,
const char *buf,
const jsmntok_t *result,
struct decodable *decodable)
{
struct secret secret;
struct json_stream *response;
const jsmntok_t *secrettok;

secrettok = json_get_member(buf, result, "secret");
json_to_secret(buf, secrettok, &secret);

crypto_secretstream_xchacha20poly1305_state crypto_state;

if (tal_bytelen(decodable->emergency_recover) < ABYTES +
HEADER_LEN)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"Can't decrypt, hex is too short!");

u8 *decrypt_blob = tal_arr(tmpctx, u8,
tal_bytelen(decodable->emergency_recover) -
ABYTES -
HEADER_LEN);
/* The header part */
if (crypto_secretstream_xchacha20poly1305_init_pull(&crypto_state,
decodable->emergency_recover,
secret.data) != 0) {
return command_fail(cmd, JSONRPC2_INVALID_PARAMS, "Can't decrypt!");
}

if (crypto_secretstream_xchacha20poly1305_pull(&crypto_state, decrypt_blob,
NULL, 0,
decodable->emergency_recover +
HEADER_LEN,
tal_bytelen(decodable->emergency_recover) -
HEADER_LEN,
NULL, 0) != 0) {
return command_fail(cmd, JSONRPC2_INVALID_PARAMS, "Can't decrypt!");
}

response = jsonrpc_stream_success(cmd);
json_add_bool(response, "valid", true);
json_add_string(response, "type", decodable->type);
json_add_hex(response, "decrypted", decrypt_blob,
tal_bytelen(decrypt_blob));

return command_finished(cmd, response);
}

static struct command_result *json_decode(struct command *cmd,
const char *buffer,
const jsmntok_t *params)
Expand Down Expand Up @@ -1036,6 +1134,17 @@ static struct command_result *json_decode(struct command *cmd,
}
if (decodable->rune)
json_add_rune(cmd, response, decodable->rune);
if (decodable->emergency_recover) {
struct out_req *req;

req = jsonrpc_request_start(cmd->plugin, cmd, "makesecret",
after_makesecret, &forward_error,
decodable);

json_add_string(req->js, "string", "scb secret");
return send_outreq(cmd->plugin, req);
}

return command_finished(cmd, response);
}

Expand Down
16 changes: 16 additions & 0 deletions tests/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -1343,6 +1343,22 @@ def test_funding_reorg_get_upset(node_factory, bitcoind):
assert only_one(l2.rpc.listpeerchannels()['channels'])['state'] == 'AWAITING_UNILATERAL'


def test_decode(node_factory, bitcoind):
"""Test the decode option to decode the contents of emergency recovery.
"""
l1 = node_factory.get_node(allow_broken_log=True)
cmd_line = ["tools/hsmtool", "getemergencyrecover", os.path.join(l1.daemon.lightning_dir, TEST_NETWORK, "emergency.recover")]
out = subprocess.check_output(cmd_line).decode('utf-8')
bech32_out = out.strip('\n')
assert bech32_out.startswith('clnemerg1')

x = l1.rpc.decode(bech32_out)

assert x["valid"]
assert x["type"] == "emergency recover"
assert x["decrypted"].startswith('17')


@unittest.skipIf(os.getenv('TEST_DB_PROVIDER', 'sqlite3') != 'sqlite3', "deletes database, which is assumed sqlite3")
def test_recover(node_factory, bitcoind):
"""Test the recover option
Expand Down
2 changes: 1 addition & 1 deletion tools/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ tools/headerversions: $(FORCE) tools/headerversions.o libccan.a
tools/headerversions.o: ccan/config.h
tools/check-bolt: tools/check-bolt.o $(TOOLS_COMMON_OBJS)

tools/hsmtool: tools/hsmtool.o $(TOOLS_COMMON_OBJS) $(BITCOIN_OBJS) common/amount.o common/autodata.o common/bech32.o common/bigsize.o common/codex32.o common/configdir.o common/configvar.o common/derive_basepoints.o common/descriptor_checksum.o common/hsm_encryption.o common/node_id.o common/type_to_string.o common/version.o wire/fromwire.o wire/towire.o
tools/hsmtool: tools/hsmtool.o $(TOOLS_COMMON_OBJS) $(BITCOIN_OBJS) common/amount.o common/autodata.o common/bech32.o common/bech32_util.o common/bigsize.o common/codex32.o common/configdir.o common/configvar.o common/derive_basepoints.o common/descriptor_checksum.o common/hsm_encryption.o common/node_id.o common/type_to_string.o common/version.o wire/fromwire.o wire/towire.o

tools/lightning-hsmtool: tools/hsmtool
cp $< $@
Expand Down
Loading
Loading