Every time I have to change the mandatory password for web apps I have to go to an online solution and then store that password somewhere in notes. (I know tools like 1Password exist but I haven’t used them till now). So I decided to write a cli tool which can generate a password for me and store it in a file on my disk.
I am currently learning the zig programming language and decided to use the same. The code for this would be simple and we will follow these steps -
- Ask user for username/email
- Ask user for domain for which to generate the password
- Generate the random password
- Store the username, domain and password combination in the file
We will also learn some of the implementation details of zig while building this small cli tool.
Prerequisites
Before we begin, make sure you have zig installed on your system. I have installed it with the help of asdf. I use asdf as it’s very convenient in maintaining various versions of tools I use.
If you want to use asdf, install it using this link - https://asdf-vm.com/guide/getting-started.html
Or you can follow the official zig guide for installation. https://ziglang.org/learn/getting-started/#installing-zig
On mac - brew install zig should do the trick.
Getting Started
To get started create a directory in your development folder - zpassword
$ mkdir zpassword
# Then cd to zpassword
$ cd zpassword
# Now let’s initialise a new project in ziglang
$ zig init-exe
The directory structure should look like
|---build.zig
|---src
|---|---main.zig
2 directories, 2 files
Let’s start changing the main.zig. Delete all the content of main.zig and leave this basic structure.
const std = @import("std");
pub fn main() !void {
}
I would like only these 62 characters be part of the password so let's add charset for base62
const std = @import("std");
const allocator = std.heap.page_allocator;
const RndGen = std.rand.DefaultPrng;
const charset: [62]u8 = [_]u8{ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
pub fn main() !void {
}
Let’s add code to take input from the user. Here I have restricted the username and domain input to be max size of 512.
const stdin = std.io.getStdIn().reader();
std.debug.print("Enter username: ", .{});
var usernameResult = try stdin.readUntilDelimiterOrEofAlloc(allocator, '\n', 512);
var username = usernameResult.?;
defer allocator.free(username);
// take domain as input
std.debug.print("Enter domain: ", .{});
var domainResult = try stdin.readUntilDelimiterOrEofAlloc(allocator, '\n', 512);
var domain = domainResult.?;
defer allocator.free(domain);
Most of the languages won’t require manual memory management but in zig we have to manage memory ourselves. It gives some freedom to use a particular type of memory allocator for a particular use case. Read more about memory allocator in my earlier post What's a memory allocator anyway?
Similar to golang, zig also provides defer which is used to execute a statement while exiting the current block. This will make sure that allocated memory gets free when current block of code exits.
We will store the password in a file name zpass.conf inside the .conf folder under the home directory.
var homeDir = std.os.getenv("HOME").?;
var confDir = ".config";
var confFile = "zpass.conf";
// Allocate memory for the dynamic string
const fullPath = try std.fmt.allocPrint(allocator, "{s}/{s}/{s}", .{ homeDir, confDir, confFile });
defer allocator.free(fullPath);
Now we will try to generate a random number using a seed value generated from slice of bytes
// Generate random seed by picking randombyte from memory
var seed: u64 = undefined;
std.os.getrandom(std.mem.asBytes(&seed)) catch unreachable;
var rnd = RndGen.init(seed);
std.mem.asBytes - /// Given a pointer to a single item, returns a slice of the underlying bytes, preserving pointer attributes.
Let’s now generate a password of length 10.
// Generate Password
var password: [10]u8 = undefined;
for (password) |*char| {
var some_random_num = rnd.random().intRangeLessThan(usize, 0, charset.len);
char.* = charset[some_random_num];
}
std.debug.print("Password: {s}\n", .{password});
Now we will open the file to write the password with username and domain. Here we are seeking to file to position 0 so that we can append the content at the beginning of the file.
// Open file to write username, domain and password
const openFlags = std.fs.File.OpenFlags{ .mode = std.fs.File.OpenMode.read_write };
var file = try std.fs.openFileAbsolute(fullPath, openFlags);
defer file.close();
// seeking file position so that to append at beginning
try file.seekTo(0);
var fullText = try std.fmt.allocPrint(allocator, "Username: {s}, Domain: {s}, Password: {s}", .{ username, domain, password });
defer allocator.free(fullText);
_ = try file.writeAll(fullText[0..]);
Add a new line character to separate it from any earlier lines already stored in the file.
// Adding new line char at end
const newline = [_]u8{'\n'};
_ = try file.write(newline[0..]);
Running the Program
$ zig build run
You will be prompted to enter your username and domain. After entering the required information, the program will generate a password and store it, along with the username and domain, in the configuration file.
Enter username: user@example.com
Enter domain: gmail.com
Password: 3zvSlZSUHL
Complete Code
const std = @import("std");
const allocator = std.heap.page_allocator;
const RndGen = std.rand.DefaultPrng;
const charset: [62]u8 = [_]u8{ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
pub fn main() !void {
const stdin = std.io.getStdIn().reader();
std.debug.print("Enter username: ", .{});
var usernameResult = try stdin.readUntilDelimiterOrEofAlloc(allocator, '\n', 512);
var username = usernameResult.?;
defer allocator.free(username);
// take domain as input
std.debug.print("Enter domain: ", .{});
var domainResult = try stdin.readUntilDelimiterOrEofAlloc(allocator, '\n', 512);
var domain = domainResult.?;
defer allocator.free(domain);
var homeDir = std.os.getenv("HOME").?;
var confDir = ".config";
var confFile = "zpass.conf";
// Allocate memory for the dynamic string
const fullPath = try std.fmt.allocPrint(allocator, "{s}/{s}/{s}", .{ homeDir, confDir, confFile });
defer allocator.free(fullPath);
// Generate random seed by picking randombyte from memory
var seed: u64 = undefined;
std.os.getrandom(std.mem.asBytes(&seed)) catch unreachable;
var rnd = RndGen.init(seed);
// Generate Password
var password: [10]u8 = undefined;
for (password) |*char| {
var some_random_num = rnd.random().intRangeLessThan(usize, 0, charset.len);
char.* = charset[some_random_num];
}
std.debug.print("Password: {s}\n", .{password});
// Open file to write username, domain and password
const openFlags = std.fs.File.OpenFlags{ .mode = std.fs.File.OpenMode.read_write };
var file = try std.fs.openFileAbsolute(fullPath, openFlags);
defer file.close();
// seeking file position so that to append at beginning
try file.seekTo(0);
var fullText = try std.fmt.allocPrint(allocator, "Username: {s}, Domain: {s}, Password: {s}", .{ username, domain, password });
defer allocator.free(fullText);
_ = try file.writeAll(fullText[0..]);
// Adding new line char at end
const newline = [_]u8{'\n'};
_ = try file.write(newline[0..]);
}
GitHub Repository
You can find the complete code for the password generator in Zig on the following GitHub repository: Zig Password Generator
Please note that storing passwords in plain text files is not secure and should not be used in real-world scenarios. This demonstration is solely for educational purposes.
Originally posted on https://sumofbytes.com/writing-a-local-password-generator-in-zig-and-storing-it-in-a-config-file-2
Top comments (0)