230 lines
10 KiB
Zig
230 lines
10 KiB
Zig
const std = @import("std");
|
|
|
|
const pbkdf2_iterations = 1000000; // https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2
|
|
pub const salt_length = 256 / 8; // https://crypto.stackexchange.com/a/56132
|
|
pub const encoded_salt_length = std.base64.standard.Encoder.calcSize(salt_length);
|
|
pub const key_length = std.crypto.aead.salsa_poly.XSalsa20Poly1305.key_length;
|
|
pub const encoded_key_length = std.base64.standard.Encoder.calcSize(key_length);
|
|
pub const nonce_length = std.crypto.aead.salsa_poly.XSalsa20Poly1305.nonce_length;
|
|
|
|
/// Generates a random salt of appropriate length
|
|
pub fn randomSalt(salt: *[salt_length]u8) void {
|
|
std.crypto.random.bytes(salt);
|
|
}
|
|
|
|
/// Generates a random salt of appropriate length, encoded into ASCII
|
|
pub fn randomEncodedSalt(encoded_salt: *[encoded_salt_length]u8) void {
|
|
var salt: [salt_length]u8 = undefined;
|
|
randomSalt(salt[0..]);
|
|
_ = std.base64.standard.Encoder.encode(encoded_salt, salt[0..]);
|
|
}
|
|
|
|
/// Generates a random key of appropriate length
|
|
pub fn randomKey(key: *[key_length]u8) void {
|
|
std.crypto.random.bytes(key);
|
|
}
|
|
|
|
/// Generates a random key of appropriate length, encoded into ASCII
|
|
pub fn randomEncodedKey(encoded_key: *[encoded_key_length]u8) void {
|
|
var key: [key_length]u8 = undefined;
|
|
randomKey(key[0..]);
|
|
_ = std.base64.standard.Encoder.encode(encoded_key, key[0..]);
|
|
}
|
|
|
|
/// Decodes key from encoded version
|
|
pub fn decodeKey(key: *[key_length]u8, encoded_key: [encoded_key_length]u8) !void {
|
|
try std.base64.standard.Decoder.decode(key, encoded_key[0..]);
|
|
}
|
|
|
|
// Derives key bytes given a plain text password and salt. It is recommended
|
|
// to use randomSalt to generate a salt - for storage, recommend a suitable ASCII encoding
|
|
pub fn deriveKey(derived_key: *[key_length]u8, password: []const u8, salt: []const u8) !void {
|
|
// Derive key using PBKDF2
|
|
try std.crypto.pwhash.pbkdf2(derived_key[0..], password, salt, pbkdf2_iterations, std.crypto.auth.hmac.sha2.HmacSha256);
|
|
}
|
|
|
|
// Derives key bytes given a plain text password and ascii encoded salt.
|
|
// Enables encryption with a single line of code, e.g.
|
|
// data = try encrypt(allocator, try deriveKeyFromEncodedSalt(password, salt), message);
|
|
//
|
|
// and decryption with:
|
|
//
|
|
// message = try decrypt(allocator, try deriveKeyFromEncodedSalt(password, salt) data);
|
|
pub fn deriveKeyFromEncodedSalt(derived_key: *[key_length]u8, password: []const u8, encoded_salt: []const u8) ![key_length]u8 {
|
|
var salt: [salt_length]u8 = undefined;
|
|
try std.base64.standard.Decoder.decode(&salt, encoded_salt);
|
|
try deriveKey(derived_key, password, salt[0..]);
|
|
return derived_key.*;
|
|
}
|
|
|
|
/// Encrypts data. Use deriveKey function to get a key from password/salt.
|
|
/// Uses a random nonce. To supply a nonce instead, use encryptWithNonce
|
|
/// Caller owns memory
|
|
pub fn encrypt(allocator: std.mem.Allocator, key: [key_length]u8, plaintext: []const u8) ![]const u8 {
|
|
var nonce: [std.crypto.aead.salsa_poly.XSalsa20Poly1305.nonce_length]u8 = undefined;
|
|
std.crypto.random.bytes(&nonce); // add nonce to beginning of our ciphertext
|
|
return try encryptWithNonce(allocator, key, nonce, plaintext);
|
|
}
|
|
|
|
pub fn encryptWithNonce(allocator: std.mem.Allocator, key: [key_length]u8, nonce: [nonce_length]u8, plaintext: []const u8) ![]const u8 {
|
|
var ciphertext = try allocator.alloc(
|
|
u8,
|
|
nonce_length + std.crypto.aead.salsa_poly.XSalsa20Poly1305.tag_length + plaintext.len,
|
|
);
|
|
errdefer allocator.free(ciphertext);
|
|
|
|
// Create the nonce
|
|
@memcpy(ciphertext[0..nonce_length], nonce[0..]); // add nonce to beginning of our ciphertext
|
|
const nonce_copy = ciphertext[0..nonce_length];
|
|
const tag = ciphertext[nonce_length .. nonce_length + std.crypto.aead.salsa_poly.XSalsa20Poly1305.tag_length];
|
|
const c = ciphertext[nonce_length + std.crypto.aead.salsa_poly.XSalsa20Poly1305.tag_length ..];
|
|
|
|
// Do the encryption
|
|
std.crypto.aead.salsa_poly.XSalsa20Poly1305.encrypt(
|
|
c,
|
|
tag,
|
|
plaintext,
|
|
"ad",
|
|
nonce_copy.*,
|
|
key,
|
|
);
|
|
return ciphertext;
|
|
}
|
|
|
|
/// Encrypts data. Use deriveKey function to get a key from password/salt
|
|
/// Caller owns memory
|
|
pub fn encryptAndEncode(allocator: std.mem.Allocator, key: [key_length]u8, plaintext: []const u8) ![]const u8 {
|
|
const ciphertext = try encrypt(allocator, key, plaintext);
|
|
defer allocator.free(ciphertext);
|
|
const Encoder = std.base64.standard.Encoder;
|
|
var encoded_ciphertext = try allocator.alloc(u8, Encoder.calcSize(ciphertext.len));
|
|
errdefer allocator.free(encoded_ciphertext);
|
|
return Encoder.encode(encoded_ciphertext, ciphertext);
|
|
}
|
|
|
|
/// Encrypts data. Use deriveKey function to get a key from password/salt
|
|
/// Caller owns memory
|
|
pub fn encryptAndEncodeWithNonce(allocator: std.mem.Allocator, key: [key_length]u8, nonce: [nonce_length]u8, plaintext: []const u8) ![]const u8 {
|
|
const ciphertext = try encryptWithNonce(allocator, key, nonce, plaintext);
|
|
defer allocator.free(ciphertext);
|
|
const Encoder = std.base64.standard.Encoder;
|
|
var encoded_ciphertext = try allocator.alloc(u8, Encoder.calcSize(ciphertext.len));
|
|
errdefer allocator.free(encoded_ciphertext);
|
|
return Encoder.encode(encoded_ciphertext, ciphertext);
|
|
}
|
|
|
|
/// Decrypts data. Use deriveKey function to get a key from password/salt
|
|
pub fn decrypt(allocator: std.mem.Allocator, key: [key_length]u8, ciphertext: []const u8) ![]const u8 {
|
|
var plaintext = try allocator.alloc(
|
|
u8,
|
|
ciphertext.len - nonce_length - std.crypto.aead.salsa_poly.XSalsa20Poly1305.tag_length,
|
|
);
|
|
errdefer allocator.free(plaintext);
|
|
const nonce = ciphertext[0..nonce_length].*;
|
|
const tag = ciphertext[nonce_length .. nonce_length + std.crypto.aead.salsa_poly.XSalsa20Poly1305.tag_length].*;
|
|
const c = ciphertext[nonce_length + std.crypto.aead.salsa_poly.XSalsa20Poly1305.tag_length ..];
|
|
|
|
try std.crypto.aead.salsa_poly.XSalsa20Poly1305.decrypt(
|
|
plaintext,
|
|
c,
|
|
tag,
|
|
"ad",
|
|
nonce,
|
|
key,
|
|
);
|
|
return plaintext;
|
|
}
|
|
|
|
/// Decrypts encoded data. Does the reverse of encryptAndEncode
|
|
/// Caller owns memory
|
|
pub fn decodeAndDecrypt(allocator: std.mem.Allocator, key: [key_length]u8, encoded_ciphertext: []const u8) ![]const u8 {
|
|
const Decoder = std.base64.standard.Decoder;
|
|
const ciphertext_len = try Decoder.calcSizeForSlice(encoded_ciphertext);
|
|
var ciphertext = try allocator.alloc(u8, ciphertext_len);
|
|
defer allocator.free(ciphertext);
|
|
try std.base64.standard.Decoder.decode(ciphertext, encoded_ciphertext);
|
|
return try decrypt(allocator, key, ciphertext);
|
|
}
|
|
|
|
// This is a pretty long running test...
|
|
// test "can encrypt and decrypt data with simpler api" {
|
|
// const allocator = std.testing.allocator;
|
|
// const plaintext = "Hello, Zig!";
|
|
// const password = "mySecurePassword";
|
|
// var key: [key_length]u8 = undefined;
|
|
// var salt: [encoded_salt_length]u8 = undefined;
|
|
// randomEncodedSalt(salt[0..]);
|
|
//
|
|
// const ciphertext = try encrypt(allocator, try deriveKeyFromEncodedSalt(&key, password, salt[0..]), plaintext);
|
|
// defer allocator.free(ciphertext);
|
|
// std.log.debug("Ciphertext: {s}\n", .{std.fmt.fmtSliceHexLower(ciphertext)});
|
|
// const decrypted_text = try decrypt(allocator, key, ciphertext);
|
|
// defer allocator.free(decrypted_text);
|
|
// try std.testing.expectEqualStrings(plaintext, decrypted_text[0..]);
|
|
// }
|
|
|
|
test "can encrypt and decrypt data with simpler api but without KDF" {
|
|
const allocator = std.testing.allocator;
|
|
const plaintext = "Hello, Zig!";
|
|
var key: [key_length]u8 = undefined;
|
|
var encoded_key: [encoded_key_length]u8 = undefined;
|
|
randomEncodedKey(encoded_key[0..]);
|
|
// std.testing.log_level = .debug;
|
|
std.log.debug("Encoded key: {s}", .{encoded_key});
|
|
|
|
try decodeKey(&key, encoded_key);
|
|
const ciphertext = try encrypt(allocator, key, plaintext);
|
|
defer allocator.free(ciphertext);
|
|
std.log.debug("Ciphertext: {s}\n", .{std.fmt.fmtSliceHexLower(ciphertext)});
|
|
const decrypted_text = try decrypt(allocator, key, ciphertext);
|
|
defer allocator.free(decrypted_text);
|
|
try std.testing.expectEqualStrings(plaintext, decrypted_text[0..]);
|
|
}
|
|
|
|
test "can encrypt twice with same result" {
|
|
const allocator = std.testing.allocator;
|
|
const plaintext = "Hello, Zig!";
|
|
var key: [key_length]u8 = undefined;
|
|
var nonce: [nonce_length]u8 = undefined;
|
|
var encoded_key: [encoded_key_length]u8 = undefined;
|
|
randomEncodedKey(encoded_key[0..]);
|
|
std.crypto.random.bytes(&nonce);
|
|
// std.testing.log_level = .debug;
|
|
std.log.debug("Encoded key: {s}", .{encoded_key});
|
|
|
|
try decodeKey(&key, encoded_key);
|
|
const ciphertext = try encryptWithNonce(allocator, key, nonce, plaintext);
|
|
defer allocator.free(ciphertext);
|
|
std.log.debug("Ciphertext: {s}\n", .{std.fmt.fmtSliceHexLower(ciphertext)});
|
|
|
|
const ciphertext2 = try encryptWithNonce(allocator, key, nonce, plaintext);
|
|
defer allocator.free(ciphertext2);
|
|
std.log.debug("Ciphertext: {s}\n", .{std.fmt.fmtSliceHexLower(ciphertext2)});
|
|
try std.testing.expectEqualSlices(u8, ciphertext, ciphertext2);
|
|
}
|
|
|
|
// test "can encrypt and decrypt data" {
|
|
// var tag: [std.crypto.aead.salsa_poly.XSalsa20Poly1305.tag_length]u8 = undefined;
|
|
// const password = "mySecurePassword";
|
|
//
|
|
// var salt: [salt_length]u8 = undefined;
|
|
// randomSalt(salt[0..]);
|
|
//
|
|
// // Derive key using PBKDF2
|
|
// var derived_key: [std.crypto.aead.salsa_poly.XSalsa20Poly1305.key_length]u8 = undefined;
|
|
// try std.crypto.pwhash.pbkdf2(&derived_key, password, &salt, pbkdf2_iterations, std.crypto.auth.hmac.sha2.HmacSha256);
|
|
//
|
|
// var nonce: [std.crypto.aead.salsa_poly.XSalsa20Poly1305.nonce_length]u8 = undefined;
|
|
// std.crypto.random.bytes(&nonce);
|
|
//
|
|
// const plaintext = "Hello, Zig!";
|
|
// var ciphertext = [_]u8{0} ** plaintext.len;
|
|
// std.crypto.aead.salsa_poly.XSalsa20Poly1305.encrypt(ciphertext[0..], &tag, plaintext, "", nonce, derived_key);
|
|
// var decrypted_text = [_]u8{0} ** ciphertext.len;
|
|
// try std.crypto.aead.salsa_poly.XSalsa20Poly1305.decrypt(&decrypted_text, ciphertext[0..], tag, "", nonce, derived_key);
|
|
//
|
|
// std.log.debug("Ciphertext: {s}\n", .{std.fmt.fmtSliceHexLower(&ciphertext)});
|
|
//
|
|
// try std.testing.expectEqualStrings(plaintext, decrypted_text[0..]);
|
|
// }
|