package main
-bip_0039_words :: [?]string{
+BIP_0039_WORDS :: [?]string{
"abandon",
"ability",
"able",
"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
}
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
}
}
package main
import "core:encoding/json"
+import "core:log"
import "core:os"
import "core:strings"
}
}
-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)
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
}
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
}
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"
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 {
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
}
nc.delwin(box_message)
}
- nc.endwin()
+ nc.endwin()
+
+ delete(app.profiles_path)
}
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) {
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)
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)
}
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)
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)
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)
+ }
}
}
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"
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
}
}
-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 {
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)
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^)
}
}
}
+
+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
+}
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"
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,
}
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
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:
}
}
- 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) {