Sodium::SecureBuffer

Add State and transitions.
  New Exceptions.
  Raise instead of crashing when attempting to access buffer.
  Allow wiping more than once.
  Add specs.
master
Didactic Drunk 2019-09-01 02:51:48 -07:00
parent 428907318b
commit 8cdb4cbf42
2 changed files with 153 additions and 0 deletions

View File

@ -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

View File

@ -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