From 8cdb4cbf42395caa9cfdcec2ac623f2855f8a2a4 Mon Sep 17 00:00:00 2001 From: Didactic Drunk <1479616+didactic-drunk@users.noreply.github.com> Date: Sun, 1 Sep 2019 02:51:48 -0700 Subject: [PATCH] Sodium::SecureBuffer Add State and transitions. New Exceptions. Raise instead of crashing when attempting to access buffer. Allow wiping more than once. Add specs. --- spec/sodium/secure_buffer_spec.cr | 84 +++++++++++++++++++++++++++++++ src/sodium/secure_buffer.cr | 69 +++++++++++++++++++++++++ 2 files changed, 153 insertions(+) create mode 100644 spec/sodium/secure_buffer_spec.cr diff --git a/spec/sodium/secure_buffer_spec.cr b/spec/sodium/secure_buffer_spec.cr new file mode 100644 index 0000000..fb707f7 --- /dev/null +++ b/spec/sodium/secure_buffer_spec.cr @@ -0,0 +1,84 @@ +require "../spec_helper" +require "../../src/sodium/secure_buffer" + +describe Sodium::SecureBuffer do + it "allocates empty" do + buf = Sodium::SecureBuffer.new 5 + buf.to_slice.each do |b| + b.should eq 0xdb_u8 + end + + buf.noaccess + buf.readonly + buf.readwrite + end + + it "allocates random" do + buf = Sodium::SecureBuffer.random 5 + buf.to_slice.bytesize.should eq 5 + buf.wipe + end + + it "copies and erases" do + bytes = Bytes.new(5) { 1_u8 } + + buf = Sodium::SecureBuffer.new bytes, erase: true + buf.to_slice.bytesize.should eq 5 + buf.to_slice.each do |b| + b.should eq 1_u8 + end + + bytes.to_slice.each do |b| + b.should eq 0_u8 + end + end + + it "dups without crashing" do + buf = Sodium::SecureBuffer.new 5 + buf.readwrite + + buf2 = buf.dup + buf2.readonly + + buf[0] = 0_u8 + end + + it "transitions correctly" do + buf = Sodium::SecureBuffer.new 5 + + buf.noaccess + buf.@state.should eq Sodium::SecureBuffer::State::Noaccess + buf.readonly { } + buf.@state.should eq Sodium::SecureBuffer::State::Noaccess + + buf.readonly + buf.@state.should eq Sodium::SecureBuffer::State::Readonly + buf.readwrite { } + buf.@state.should eq Sodium::SecureBuffer::State::Readonly + + buf.readwrite + buf.@state.should eq Sodium::SecureBuffer::State::Readwrite + + buf.wipe + buf.@state.should eq Sodium::SecureBuffer::State::Wiped + end + + it "can wipe more than once" do + buf = Sodium::SecureBuffer.new 5 + 3.times { buf.wipe } + end + + it "can't transition from wiped" do + buf = Sodium::SecureBuffer.new 5 + buf.wipe + expect_raises Sodium::SecureBuffer::Error::KeyWiped do + buf.readwrite + end + expect_raises Sodium::SecureBuffer::Error::KeyWiped do + buf.readonly + end + expect_raises Sodium::SecureBuffer::Error::KeyWiped do + buf.noaccess + end + end +end diff --git a/src/sodium/secure_buffer.cr b/src/sodium/secure_buffer.cr index 859342b..34abb8c 100644 --- a/src/sodium/secure_buffer.cr +++ b/src/sodium/secure_buffer.cr @@ -4,6 +4,23 @@ require "./wipe" module Sodium # Allocate guarded memory using [sodium_malloc](https://libsodium.gitbook.io/doc/memory_management) class SecureBuffer + class Error < Sodium::Error + class KeyWiped < Error + end + + class InvalidStateTransition < Error + end + end + + enum State + Readwrite + Readonly + Noaccess + Wiped + end + + @state = State::Readwrite + getter bytesize delegate :+, :[], :[]=, to: to_slice @@ -37,8 +54,11 @@ module Sodium end def wipe + return if @state == State::Wiped readwrite Sodium.memzero self.to_slice + @state = State::Wiped + noaccess! end def finalize @@ -46,7 +66,9 @@ module Sodium end # Returns key + # May permanently set key to readonly depending on class usage. def to_slice + readonly if @state == State::Noaccess Slice(UInt8).new @ptr, @bytesize end @@ -58,8 +80,30 @@ module Sodium self.class.new self end + # Temporarily make buffer readonly within the block returning to the prior state on exit. + def readonly + with_state State::Readonly do + yield + end + end + + # Temporarily make buffer readonly within the block returning to the prior state on exit. + def readwrite + with_state State::Readwrite do + yield + end + end + # Makes a region allocated using sodium_malloc() or sodium_allocarray() inaccessible. It cannot be read or written, but the data are preserved. def noaccess + raise Error::KeyWiped.new if @state == State::Wiped + noaccess! + @state = State::Noaccess + self + end + + # Also used by #wipe + private def noaccess! if LibSodium.sodium_mprotect_noaccess(@ptr) != 0 raise "sodium_mprotect_noaccess" end @@ -68,17 +112,21 @@ module Sodium # Marks a region allocated using sodium_malloc() or sodium_allocarray() as read-only. def readonly + raise Error::KeyWiped.new if @state == State::Wiped if LibSodium.sodium_mprotect_readonly(@ptr) != 0 raise "sodium_mprotect_readonly" end + @state = State::Readonly self end # Marks a region allocated using sodium_malloc() or sodium_allocarray() as readable and writable, after having been protected using sodium_mprotect_readonly() or sodium_mprotect_noaccess(). def readwrite + raise Error::KeyWiped.new if @state == State::Wiped if LibSodium.sodium_mprotect_readwrite(@ptr) != 0 raise "sodium_mprotect_readwrite" end + @state = State::Readwrite self end @@ -91,5 +139,26 @@ module Sodium def ==(other : Bytes) Sodium.memcmp self.to_slice, other end + + private def set_state(new_state : State) + return if @state == new_state + + case new_state + when State::Readwrite; readwrite + when State::Readonly ; readonly + when State::Noaccess ; noaccess + when State::Wiped ; raise Error::InvalidStateTransition.new + else + raise "unknown state #{new_state}" + end + end + + private def with_state(new_state : State) + old_state = @state + set_state new_state + yield + ensure + set_state old_state if old_state + end end end