]> jxnshi.xyz Git - mesange.git/commitdiff
Working on seed phrase
authorjxnshi <jxnshi@cock.li>
Fri, 3 Jan 2025 22:00:34 +0000 (23:00 +0100)
committerjxnshi <jxnshi@cock.li>
Fri, 3 Jan 2025 22:00:34 +0000 (23:00 +0100)
client-cli/bip_0039.odin
client-cli/client-cli
client-cli/command.odin
client-cli/config.odin
client-cli/main.odin
client-cli/profile.odin
client-cli/state.odin

index c80dca7b5769b7c70ecae7db3e689664e0609220..9d2e75d2869fbbee799e981dd699d63d07a9d270 100644 (file)
@@ -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
 }
index 00379315ca18186bcb67cc6180e65127181be0c9..46817381c3f8315b4078576f793221b83d667111 100755 (executable)
Binary files a/client-cli/client-cli and b/client-cli/client-cli differ
index 749181a46dbed5f3e0377cbabef7f0f465237199..09c6cbc20b7f66a76858a0e567c67ae4b81c0d72 100644 (file)
@@ -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
         }
     }
 
index 4079819143ad56e2a9b50279fe80af5edc7783c7..f9359eb04626ae6fcad0704c7b0fe99cef0b4734 100644 (file)
@@ -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
     }
index c3c163004e14aeb07bf84349b474bfc9e247a19d..eb4c0ced9fbd9e421e15fdad70727918b3c3d00f 100644 (file)
@@ -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)
+        }
     }
 }
index 238fae5c08c25c1b0c5ba2c7915b5319f3124f0a..ee307d4f621ab9b8ef561903e5e7a4f2ae92e194 100644 (file)
@@ -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
+}
index 783cdb8d2004cff9fa1161bef6ff31c41071e3b2..125f9280f415e4edcd8908d10925d6fb35523f8b 100644 (file)
@@ -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..<WORD_PER_LINE {
+                word_buffer := line_buffer[(WORD_LEN + 1) * j:][:WORD_LEN]
+
+                number := i * WORD_PER_LINE + j + 1
+
+                if number <= 9 {
+                    _ = fmt.bprintf(word_buffer[1:2], "%v", number)
+                } else {
+                    _ = fmt.bprintf(word_buffer[:2], "%v", number)
+                }
+
+                word_buffer[2] = '.'
+
+                entropy_flag: u128 = 0xFFE00000 << (32 * 3)
+                word_index_shifted := entropy & entropy_flag
+
+                word_index := u16(word_index_shifted >> (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) {