From: jxnshi Date: Fri, 3 Jan 2025 22:00:34 +0000 (+0100) Subject: Working on seed phrase X-Git-Url: https://jxnshi.xyz/repos?a=commitdiff_plain;h=08950a7abb96d5cb75fd06fbb123dfec5b65d34a;p=mesange.git Working on seed phrase --- diff --git a/client-cli/bip_0039.odin b/client-cli/bip_0039.odin index c80dca7..9d2e75d 100644 --- a/client-cli/bip_0039.odin +++ b/client-cli/bip_0039.odin @@ -1,6 +1,6 @@ package main -bip_0039_words :: [?]string{ +BIP_0039_WORDS :: [?]string{ "abandon", "ability", "able", @@ -2051,14 +2051,14 @@ bip_0039_words :: [?]string{ "zoo", } -bip_0039_max_word_len :: proc() -> int { - max_len: int +BIP_0039_MAX_WORD_LEN :: 8 - for word in bip_0039_words { - if len(word) > max_len { - max_len = len(word) +bip_0039_get_word_index :: proc(word: string) -> Maybe(int) { + for bip_word, i in BIP_0039_WORDS { + if word == bip_word { + return i } } - return max_len + return nil } diff --git a/client-cli/client-cli b/client-cli/client-cli index 0037931..4681738 100755 Binary files a/client-cli/client-cli and b/client-cli/client-cli differ diff --git a/client-cli/command.odin b/client-cli/command.odin index 749181a..09c6cbc 100644 --- a/client-cli/command.odin +++ b/client-cli/command.odin @@ -14,16 +14,32 @@ handle_command :: proc(app: ^App, command: string) -> bool { case .Invalid_Config: switch command { case ":r", ":retry": + return true + case ":del", ":delete": config_path := fpath.join({ app.storage_path, "config.json" }, context.temp_allocator) - os.remove_directory(config_path) + os.remove(config_path) + + return true } + case .Invalid_Profile: switch command { case ":r", ":retry": + return true + case ":del", ":delete": profile_path := app_get_profile_path(app, context.temp_allocator) - os.remove_directory(profile_path) + os.remove(profile_path) + + return true + } + + case .Ask_Profile_Seed_Phrase: + switch command { + case ":reg", ":regen": + app_reset_seed_phrase(app) + return true } } diff --git a/client-cli/config.odin b/client-cli/config.odin index 4079819..f9359eb 100644 --- a/client-cli/config.odin +++ b/client-cli/config.odin @@ -1,6 +1,7 @@ package main import "core:encoding/json" +import "core:log" import "core:os" import "core:strings" @@ -37,7 +38,7 @@ config_to_parsed :: proc(config: Config) -> Config_Parsed { } } -config_load :: proc(app: ^App) -> (Config, Config_Load_Error) { +config_load :: proc(app: ^App) -> (Config, Maybe(Config_Load_Error)) { config_path := fpath.join({ app.storage_path, "config.json" }, context.temp_allocator) raw_config, config_read := os.read_entire_file_from_filename(config_path) @@ -64,11 +65,12 @@ config_deinit :: proc(config: Config) { config_update :: proc(config: Config, app: ^App) { config_path := fpath.join({ app.storage_path, "config.json" }, context.temp_allocator) - config_file, err1 := os.open(config_path, os.O_WRONLY | os.O_CREATE | os.O_TRUNC, 0777) + config_file, err1 := os.open(config_path, os.O_WRONLY | os.O_CREATE | os.O_TRUNC, 0o664) parsed_config := config_to_parsed(config) if err1 != nil { + log.errorf("Failed to open config file with error %v.", err1) app_set_info_bar(app, "Failed to save config.") return } @@ -77,6 +79,7 @@ config_update :: proc(config: Config, app: ^App) { err2 := json.marshal_to_writer(config_file_stream, parsed_config, &json_marshal_options) if err2 != nil { + log.errorf("Failed to marshal config with error %v.", err2) app_set_info_bar(app, "Failed to save config.") return } diff --git a/client-cli/main.odin b/client-cli/main.odin index c3c1630..eb4c0ce 100644 --- a/client-cli/main.odin +++ b/client-cli/main.odin @@ -1,9 +1,13 @@ package main +import "base:runtime" + import "core:encoding/json" +import "core:crypto/sha3" import "core:fmt" import "core:log" import "core:math/rand" +import "core:mem" import "core:net" import "core:os" import "core:strings" @@ -31,17 +35,24 @@ App :: struct { mutex: sync.Mutex, running: bool, - screen: ^nc.Window, storage_path: string, + profiles_path: string, + + screen: ^nc.Window, state: State, box_message: Maybe(^nc.Window), info_bar: ^nc.Window, input_window: ^nc.Window, + info_bar_content: string, + config: Config, profile_password: Maybe(string), profile: Profile, + + seed_phrase_entropy: u128, + seed_phrase_checksum: u8, } app_init :: proc(storage_path: string) -> App { @@ -69,27 +80,21 @@ app_init :: proc(storage_path: string) -> App { nc.refresh() - if !os.exists(storage_path) { - err := os.make_directory(storage_path) - - if err != nil { - fmt.eprintln("Failed to create storage directory.") - os.exit(1) - } - } - app := App{ running = true, - screen = screen, storage_path = storage_path, + profiles_path = fpath.join({ storage_path, "profiles" }), + + screen = screen, info_bar = info_bar, input_window = input_window, } - app_clear_info_bar(&app) + app_update_info_bar(&app) app_set_state(&app, .Load_Config) + app_reset_seed_phrase(&app) return app } @@ -104,7 +109,9 @@ app_deinit :: proc(app: ^App) { nc.delwin(box_message) } - nc.endwin() + nc.endwin() + + delete(app.profiles_path) } app_set_state :: proc(app: ^App, state: State) { @@ -112,21 +119,14 @@ app_set_state :: proc(app: ^App, state: State) { nc.refresh() app_clear_box_message(app) - app_clear_info_bar(app) + app_update_info_bar(app) app.state = state } app_set_info_bar :: proc(app: ^App, content: string) { - app_clear_info_bar(app) - - c_content := strings.clone_to_cstring(content, context.temp_allocator) - - nc.wattron(app.info_bar, nc.COLOR_PAIR(1)) - nc.wprintw(app.info_bar, c_content) - nc.wattroff(app.info_bar, nc.COLOR_PAIR(1)) - - nc.wrefresh(app.info_bar) + app.info_bar_content = content + app_update_info_bar(app) } app_set_box_message :: proc(app: ^App, lines: []string) { @@ -170,7 +170,7 @@ app_set_box_message :: proc(app: ^App, lines: []string) { sync.mutex_unlock(&app.mutex) } -app_clear_info_bar :: proc(app: ^App) { +app_update_info_bar :: proc(app: ^App) { height, width := nc.getmaxyx(app.info_bar) color := nc.COLOR_PAIR(1) @@ -181,6 +181,9 @@ app_clear_info_bar :: proc(app: ^App) { nc.waddch(app.info_bar, ' ') } + c_content := strings.clone_to_cstring(app.info_bar_content, context.temp_allocator) + nc.mvwprintw(app.info_bar, 0, 0, c_content) + nc.wattroff(app.info_bar, color) nc.wrefresh(app.info_bar) } @@ -198,33 +201,59 @@ app_clear_box_message :: proc(app: ^App) { app_get_input :: proc(app: ^App, allocator := context.allocator) -> string { context.allocator = allocator - nc.wclear(app.input_window) - nc.wrefresh(app.input_window) + for { + nc.wclear(app.input_window) + nc.wrefresh(app.input_window) - buffer: [256]u8 + buffer: [256]u8 - screen_height, screen_width := nc.getmaxyx(app.screen) + screen_height, screen_width := nc.getmaxyx(app.screen) - nc.curs_set(1) - nc.wgetnstr(app.input_window, raw_data(buffer[:]), len(buffer)) - nc.curs_set(0) + nc.curs_set(1) + nc.wgetnstr(app.input_window, raw_data(buffer[:]), len(buffer)) + nc.curs_set(0) - nc.wclear(app.input_window) + nc.wclear(app.input_window) - return strings.clone_from_cstring(cstring(raw_data(&buffer))) + c_input := cstring(raw_data(buffer[:])) + + if len(c_input) == 0 { + continue + } + + app_set_info_bar(app, "") + + return strings.clone_from_cstring(cstring(raw_data(buffer[:]))) + } } app_get_profile_path :: proc(app: ^App, allocator := context.allocator) -> string { if _, ok := app.profile_password.?; ok { - return fpath.join({ app.storage_path, app.config.selected_profile.? }, allocator) + return fpath.join({ app.profiles_path, app.config.selected_profile.? }, allocator) } else { profile_filename := strings.concatenate({ app.config.selected_profile.?, ".json" }, allocator) - return fpath.join({ app.storage_path, profile_filename }, allocator) + return fpath.join({ app.profiles_path, profile_filename }, allocator) } } +app_reset_seed_phrase :: proc(app: ^App) { + app.seed_phrase_entropy = rand.uint128() + + sha_context: sha3.Context + sha3.init_256(&sha_context) + + sha3.update(&sha_context, mem.any_to_bytes(app.seed_phrase_entropy)) + + entropy_hash: [sha3.DIGEST_SIZE_256]u8 + sha3.final(&sha_context, entropy_hash[:]) + + app.seed_phrase_checksum = entropy_hash[0] & 0b1111 +} + handle_state :: proc(app: ^App) { for { + defer free_all(context.temp_allocator) + switch app.state { case .Load_Config: state_load_config(app) case .Invalid_Config: state_invalid_config(app) @@ -232,18 +261,31 @@ handle_state :: proc(app: ^App) { case .Load_Profile: state_load_profile(app) case .Invalid_Profile: state_invalid_profile(app) case .Ask_Profile: state_ask_profile(app) - case .Ask_Profile_Password: state_ask_profile_password(app) case .Ask_Profile_Seed_Phrase: state_ask_profile_seed_phrase(app) + case .Ask_Profile_Host: state_ask_profile_host(app) + case .Ask_Profile_Password_Protection: state_ask_profile_password_protection(app) + case .Ask_Profile_Set_Password: state_ask_profile_set_password(app) + case .Ask_Profile_Confirm_Password: state_ask_profile_confirm_password(app) + case .Ask_Profile_Password: state_ask_profile_password(app) + + case .Main: state_main(app) } } } main :: proc() { home_path := os.get_env("HOME") - storage_path := fpath.join({ home_path, "mesange" }, context.temp_allocator) + + storage_path := fpath.join({ home_path, "mesange" }) + defer delete(storage_path) + log_path := fpath.join({ storage_path, "log.txt" }, context.temp_allocator) - log_file, err := os.open(log_path, os.O_WRONLY | os.O_CREATE | os.O_TRUNC, 0777) + if !os.exists(storage_path) { + _ = os.make_directory(storage_path) + } + + log_file, err := os.open(log_path, os.O_WRONLY | os.O_CREATE | os.O_APPEND, 0o664) defer os.close(log_file) context.logger = log.create_file_logger(log_file) @@ -284,10 +326,17 @@ main :: proc() { app := app_init(storage_path) defer app_deinit(&app) - handle_state_thread := thread.create_and_start_with_poly_data(&app, handle_state, context) + handle_state_thread_context := runtime.default_context() + handle_state_thread_context.logger = context.logger + handle_state_thread_context.random_generator = context.random_generator + + handle_state_thread := thread.create_and_start_with_poly_data(&app, handle_state, handle_state_thread_context) defer thread.terminate(handle_state_thread, 0) for app.running { - + defer { + time.sleep(1_000_000) + free_all(context.temp_allocator) + } } } diff --git a/client-cli/profile.odin b/client-cli/profile.odin index 238fae5..ee307d4 100644 --- a/client-cli/profile.odin +++ b/client-cli/profile.odin @@ -1,8 +1,12 @@ package main + import "core:crypto/aes" import "core:crypto/ed25519" +import "core:crypto/sha3" import "core:encoding/json" +import "core:log" import "core:math/rand" +import "core:mem" import "core:os" import "core:strings" @@ -24,6 +28,12 @@ Profile_Load_Error :: enum { Invalid_File, } +Profile_Set_Seed_Phrase_Error :: enum { + Invalid_Word_Count, + Invalid_Word, + Invalid_Checksum, +} + profile_from_parsed :: proc(parsed_profile: Profile_Parsed, allocator := context.allocator) -> Profile { parsed_profile := parsed_profile @@ -50,12 +60,12 @@ profile_to_parsed :: proc(profile: Profile) -> Profile_Parsed { } } -profile_load_from_name :: proc(name: string, app: ^App) -> (Profile, Profile_Load_Error) { +profile_load_from_name :: proc(name: string, app: ^App) -> (Profile, Maybe(Profile_Load_Error)) { buffer: [4096]u8 profile_content: string if profile_password, ok := app.profile_password.?; ok { - encrypted_profile_path := fpath.join({ app.storage_path, name }, context.temp_allocator) + encrypted_profile_path := fpath.join({ app.profiles_path, name }, context.temp_allocator) encrypted_profile_content, profile_read := os.read_entire_file_from_filename(encrypted_profile_path, context.temp_allocator) if !profile_read { @@ -76,7 +86,7 @@ profile_load_from_name :: proc(name: string, app: ^App) -> (Profile, Profile_Loa profile_content = string(buffer[:len(ciphertext)]) } else { profile_filename := strings.concatenate({ name, ".json" }, context.temp_allocator) - profile_path := fpath.join({ app.storage_path, profile_filename }, context.temp_allocator) + profile_path := fpath.join({ app.profiles_path, profile_filename }, context.temp_allocator) profile_content, profile_read := os.read_entire_file_from_filename(profile_path, context.temp_allocator) @@ -101,7 +111,7 @@ profile_deinit :: proc(profile: ^Profile) { profile_update :: proc(profile: ^Profile, app: ^App) { profile_path := app_get_profile_path(app, context.temp_allocator) - profile_file, err1 := os.open(profile_path, os.O_WRONLY | os.O_CREATE | os.O_TRUNC, 0777) + profile_file, err1 := os.open(profile_path, os.O_CREATE | os.O_TRUNC, 0o664) parsed_profile := profile_to_parsed(profile^) @@ -155,3 +165,68 @@ profile_update :: proc(profile: ^Profile, app: ^App) { } } } + +profile_set_private_key_from_seed_phrase :: proc(profile: ^Profile, seed_phrase: string) -> Maybe(Profile_Set_Seed_Phrase_Error) { + seed_phrase := seed_phrase + + bip_0039_words := BIP_0039_WORDS + + entropy: u128 + checksum: u8 + + word_count: int + + for word in strings.split_iterator(&seed_phrase, " ") { + if word_count == 12 { + return .Invalid_Word_Count + } + + word_index, ok := bip_0039_get_word_index(word).? + + if !ok { + return .Invalid_Word + } + + if word_count + 1 != 12 { + entropy |= u128(word_index) + entropy <<= 11 + } else { + entropy |= u128(word_index & 0b1111_111) + checksum |= u8((word_index >> 7) & 0b1111) + } + + word_count += 1 + } + + if word_count != 12 { + return .Invalid_Word_Count + } + + sha_context: sha3.Context + sha3.init_256(&sha_context) + + sha3.update(&sha_context, mem.any_to_bytes(entropy)) + + entropy_hash: [sha3.DIGEST_SIZE_256]u8 + sha3.final(&sha_context, entropy_hash[:]) + + found_checksum := entropy_hash[0] & 0b1111 + + log.infof("- Entropy bytes: %8b", mem.any_to_bytes(entropy)) + log.infof("- Entropy hash: %8b", mem.any_to_bytes(entropy_hash)) + log.infof("- Checksum: %4b", checksum) + log.infof("- Found checksum: %4b", found_checksum) + + if found_checksum != checksum { + return .Invalid_Checksum + } + + private_key_bytes: [sha3.DIGEST_SIZE_256]u8 + sha3.final(&sha_context, private_key_bytes[:]) + + if !ed25519.private_key_set_bytes(&profile.private_key, private_key_bytes[:]) { + log.errorf("Failed to set private key bytes.") + } + + return nil +} diff --git a/client-cli/state.odin b/client-cli/state.odin index 783cdb8..125f928 100644 --- a/client-cli/state.odin +++ b/client-cli/state.odin @@ -1,9 +1,14 @@ package main import "core:crypto/aes" +import "core:crypto/sha3" import "core:encoding/json" import "core:fmt" +import "core:log" +import "core:math/rand" +import "core:mem" import "core:os" +import "core:slice" import "core:strings" import fpath "core:path/filepath" @@ -15,8 +20,12 @@ State :: enum { Load_Profile, Invalid_Profile, Ask_Profile, - Ask_Profile_Password, Ask_Profile_Seed_Phrase, + Ask_Profile_Host, + Ask_Profile_Password_Protection, + Ask_Profile_Set_Password, + Ask_Profile_Confirm_Password, + Ask_Profile_Password, Main, } @@ -25,17 +34,15 @@ state_load_config :: proc(app: ^App) { config_path := fpath.join({ app.storage_path, "config.json" }, context.temp_allocator) raw_config, config_read := os.read_entire_file_from_filename(config_path) - config: Config - - if config_read { - err := json.unmarshal(raw_config, &config) + config, config_error := config_load(app) - if err != nil { + if err, ok := config_error.?; ok { + switch err { + case .No_File_Present: + case .Invalid_File: app_set_state(app, .Invalid_Config) return } - } else { - app_set_info_bar(app, "No config, using default.") } app.config = config @@ -82,9 +89,9 @@ state_load_profile :: proc(app: ^App) { return } - profile, err := profile_load_from_name(selected_profile, app) + profile, profile_error := profile_load_from_name(selected_profile, app) - if err != nil { + if err, ok := profile_error.?; ok { switch err { case .No_File_Present: case .Invalid_Password: @@ -146,47 +153,160 @@ state_ask_profile :: proc(app: ^App) { } } - if len(input) == 0 { - return - } - if !is_identifier(input) { app_set_info_bar(app, "Invalid profile name.") return } - app.config.selected_profile = app_get_input(app) + app.config.selected_profile = input config_update(app.config, app) app_set_state(app, .Load_Profile) } -state_ask_profile_password :: proc(app: ^App) { +state_ask_profile_seed_phrase :: proc(app: ^App) { + // Template: '10. base 11. bean 12. betray '. + WORD_LEN :: 2 + 1 + 1 + BIP_0039_MAX_WORD_LEN + WORD_LINE_LEN :: WORD_LEN + 1 + WORD_LEN + 1 + WORD_LEN + + WORD_PER_LINE :: 3 + + bip_0039_words := BIP_0039_WORDS + + { // Show seed phrase. + entropy := app.seed_phrase_entropy + + word_lines_buffer: [WORD_LINE_LEN * 4]u8 + mem.set(raw_data(word_lines_buffer[:]), ' ', len(word_lines_buffer)) + + word_lines: [4]string + + for &word_line, i in word_lines { + line_buffer := word_lines_buffer[WORD_LINE_LEN * i : WORD_LINE_LEN * (i + 1)] + + for j in 0..> (32 * 3 + 21)) + entropy <<= 11 + + if entropy == 0 { + word_index |= u16(app.seed_phrase_checksum) << 7 + } + + word := bip_0039_words[word_index] + + _ = fmt.bprintf(word_buffer[4:], "%v", word) + } + + word_line = string(line_buffer) + } + + app_set_box_message( + app, + { + "Please enter either your seed phrase", + "or the one bellow.", + "You can type :regen to generate", + "a new seed phrase.", + "", + word_lines[0], + word_lines[1], + word_lines[2], + word_lines[3], + } + ) + } + + input := app_get_input(app) + + if input[0] == ':' { + if !handle_command(app, input) { + app_set_info_bar(app, "Invalid command.") + return + } + + return + } + + err := profile_set_private_key_from_seed_phrase(&app.profile, input) + + if err != nil { + log.errorf("Invalid seed phrase with error %v", err) + app_set_info_bar(app, "Invalid seed phrase") + return + } + + profile_update(&app.profile, app) + + app_set_state(app, .Ask_Profile_Host) +} + +state_ask_profile_host :: proc(app: ^App) { app_set_box_message( app, { - "Profile is encrypted.", - "Please enter profile password." + "Enter your host.", }, ) +} - app.profile_password = app_get_input(app) +state_ask_profile_password_protection :: proc(app: ^App) { + app_set_box_message( + app, + { + "Should this profile be password protected?", + "Please type 'yes' or 'no'." + }, + ) - app_set_state(app, .Load_Profile) + input := app_get_input(app) + + switch input { + case "yes": app_set_state(app, .Ask_Profile_Set_Password) + case "no": app_set_state(app, .Ask_Profile_Seed_Phrase) + case: app_set_info_bar(app, "Invalid answer.") + } } -state_ask_profile_seed_phrase :: proc(app: ^App) { - max_seed_word_len := bip_0039_max_word_len() +state_ask_profile_set_password :: proc(app: ^App) { + app_set_box_message( + app, + { + "Please enter a password.", + }, + ) +} + +state_ask_profile_confirm_password :: proc(app: ^App) { + +} +state_ask_profile_password :: proc(app: ^App) { app_set_box_message( app, { - "No seed phrase found.", - "Please enter either your seed phrase", - "or the one bellow.", - "", - } + "Profile is encrypted.", + "Please enter profile password." + }, ) + + app.profile_password = app_get_input(app) + + app_set_state(app, .Load_Profile) } state_main :: proc(app: ^App) {