Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • redox-os/pkgar
  • Rustywolf/pkgar
  • freewilll/pkgar
  • rw_van/pkgar
  • josh/pkgar
5 results
Show changes
Commits on Source (45)
  • SamwiseFilmore's avatar
    Port to libsodium · 069b96b3
    SamwiseFilmore authored
    It looks like sodiumoxide only depends on std for serializing with
    serde, disabling default features should preserve no-std for the
    library.
    
    As I more or less don't know what I'm doing with crypto stuff, there's
    probably some bits of code in here that are super naive. Also it hasn't
    gone through much testing, so I don't know what the value is there
    either.
    
    I'd like to integrate `pkgar-keys` at some point (little project I've
    been working on to store keys for packaging and verification), but I'd
    appreciate some guidance on how those should be formatted. More
    discussion should probably happen in the repo:
    https://gitlab.redox-os.org/MggMuggins/pkgar-keys
    069b96b3
  • SamwiseFilmore's avatar
    Clean up old code/comments · 2871ad9a
    SamwiseFilmore authored
    2871ad9a
  • Jeremy Soller's avatar
    Merge branch 'libsodium' into 'master' · 060de420
    Jeremy Soller authored
    Port to libsodium
    
    See merge request !4
    060de420
  • SamwiseFilmore's avatar
    Fix size mismatch for sig; Test script · 47023719
    SamwiseFilmore authored
    I clearly never ran the tests...
    
    In addition to running the tests over my code... I updated the test
    script to allow debug builds when stuff panics and you need a backtrace,
    release builds optimize out the line numbers.
    47023719
  • Jeremy Soller's avatar
    Merge branch 'libsodium-fix' into 'master' · 24e2a959
    Jeremy Soller authored
    Libsodium fix
    
    See merge request !5
    24e2a959
  • SamwiseFilmore's avatar
    DEV: Use pkgar-keys; Error handling · 7ddec217
    SamwiseFilmore authored
    - Uses pkgar-keys to read and write keyfiles instead of just binary keys.
    - I implemented a couple of `From`s for Error, and removed many map_err
      calls around the lib. I'm sure there's plenty I missed.
    
    Note that the integration tests are failing at this point, I'm going
    to fix them separately from this branch and merge over.
    7ddec217
  • SamwiseFilmore's avatar
    712e20c4
  • SamwiseFilmore's avatar
    1a7b5d58
  • SamwiseFilmore's avatar
    Add READMEs to all crates · d22222bf
    SamwiseFilmore authored
    d22222bf
  • SamwiseFilmore's avatar
    Consistency for CLI, crate layout; manifest cleanup · 97912b2e
    SamwiseFilmore authored
    - Use --pkey and --skey in both crates.
    - Remove single bin target for pkgar; use main.rs
    - Clean up mentions of rand in pkgar
    97912b2e
  • SamwiseFilmore's avatar
    WIP: Refactor pkgar into two crates · 421aeae9
    SamwiseFilmore authored
    This compiles, but the tests fail due to an off by one or similar issue
    hiding someplace. Debugging is going to require a significant set of
    improvements to the error handling in the crate, the messages are crap
    atm.
    
    I also added the thiserror crate to make generating error impls easier,
    so it should be fairly straightforward from that side of things to
    improve the errors. Note that thiserror isn't no_std right now, so it's
    only used in pkgar, pkgar_core implements all the froms without the help
    of a macro.
    421aeae9
  • SamwiseFilmore's avatar
    Small Error improvements; Passes tests · 48dac371
    SamwiseFilmore authored
    Everything appears to be functional. I assumed that two functions that
    were called the same thing with approximately the same contents had the
    same behavior... not a safe assumption to make. That function is now
    reimplemented, among with several other small fixes. I also added a call
    to `pkgar list` in the tests to make sure it doesn't panic or do
    something else equally dumb.
    
    I also added a dependency on UserFacingError... I'm not sure how much
    value it will add, but I can see it being more useful as pkgar matures
    and good error reporting becomes more critical. Many improvements on
    that front soon I hope.
    
    In terms of the refactoring that happened last commit: There are some
    critical issues with implementing PackageSrc as a trait, namely needing
    to call read_at (which should be considered expensive) in order to read
    the header, something that most of the functions have to do. read_at
    could conceiveably cross network boundaries in the future, so it's
    important not to call it unessesarily. More design work needs to be done
    there.
    48dac371
  • SamwiseFilmore's avatar
    Add a header() function to PackageSrc; Standardize func names · e689baba
    SamwiseFilmore authored
    I'm not super happy with this design because it makes for a reasonable
    amount of ugly boilerplate for implementors of PackageSrc. Works though,
    and it's faster.
    
    TODO: Implement new type for buffers of u8s instead of impl PackageSrc
    for AsRef<[u8]>
    e689baba
  • SamwiseFilmore's avatar
    Implement PackageBuf · dd8428b4
    SamwiseFilmore authored
    dd8428b4
  • Jeremy Soller's avatar
    Merge branch 'keys-lib-cheap-header' into 'master' · 0bf77ce0
    Jeremy Soller authored
    Keys lib
    
    See merge request !6
    0bf77ce0
  • SamwiseFilmore's avatar
    pkgar-keys: Refactor Errors · faaf1c88
    SamwiseFilmore authored
    Fixed two little bugs:
    - Private key files now use 600 instead of 644
    - Fixed default key file paths
    
    Pulled in the same error deps as the main library,
    - UFE for easy pretty-printing of errors
    - thiserror for deriving std::error::Error for error types
    
    pkgar-keys now uses two error types, one that includes a file path for
    context and an ErrorKind for passing around internally and containing
    other error types. I'm still not happy with the sheer volume of
    boilerplate needed to create Error, so more work is needed, but error
    reporting is much better with these changes, and is probably more
    maintainable even given the boilerplate.
    faaf1c88
  • SamwiseFilmore's avatar
    pkgar-keys: Update seckey + refactor passwd; Docs and cleanup · 1a0408dd
    SamwiseFilmore authored
    - SecKey took a lower-level approach for its 0.11 API, so I refactored
      passwords to include a new type to make sure everything gets zeroed.
    - Bunch of documentation work, I'm happy with the state of the rustdoc
      and error handling, at least enough to release 1.0 soon.
    1a0408dd
  • SamwiseFilmore's avatar
    Bump crates; remote dbg line in keys · aa196402
    SamwiseFilmore authored
    aa196402
  • SamwiseFilmore's avatar
    Update README · fdc7a322
    SamwiseFilmore authored
    fdc7a322
  • SamwiseFilmore's avatar
    pkgar-keys: Prep for release · b36b0647
    SamwiseFilmore authored
    Just a couple of extra keys in Cargo.toml
    b36b0647
  • SamwiseFilmore's avatar
    pkgar-core: Use min_const_generics · 8e65e210
    SamwiseFilmore authored
    No more nasty handwritten debug impls.
    8e65e210
  • Jeremy Soller's avatar
    Merge branch 'keys-errors' into 'master' · 081ecf8a
    Jeremy Soller authored
    pkgar-keys errors
    
    See merge request !7
    081ecf8a
  • Jeremy Soller's avatar
    Merge branch 'min_const_generics' into 'master' · e7130b2b
    Jeremy Soller authored
    pkgar-core: Use min_const_generics
    
    See merge request !8
    e7130b2b
  • SamwiseFilmore's avatar
    WIP: Refactor extraction into transaction API · 5cf2327c
    SamwiseFilmore authored
    The tests fail with this commit, but I want to be able to branch to do
    the error refactoring to print out filesystem contents. Might end up
    amending this.
    
    Essentially the vec that was internal to the extract function is the
    backbone of a new type, Transaction, which provides semi-atomic package
    operations against the filesystem. Currently it's basically a refactor
    of extract, but it provides a more flexible place to add ugrades and
    removes (function headers implemented).
    
    This also entails a large amount of error refactoring to provide helpful
    context for various errors. More work to do here.
    5cf2327c
  • SamwiseFilmore's avatar
    Error handling "improvements"; Fix tests · 1bf25d2f
    SamwiseFilmore authored
    The tests failed because I accidentally dropped an fs::create_dir_all
    during my refactor.
    
    But in order to determine why my tests failed, I added a significant
    amount of code to add context to IO errors, as error reporting for pkgar
    is pretty critical. This needs a lot more work, but need to add features
    first.
    1bf25d2f
  • SamwiseFilmore's avatar
    Implement remove · e78fded8
    SamwiseFilmore authored
    Added remove as a transaction based on a PackageSrc; All it does is
    iterate the entries for a given package src and remove them. It checks
    the hashes to make sure that it's removing the file that was in the
    package.
    e78fded8
  • SamwiseFilmore's avatar
    Implement upgrade; Refactoring; Tests in Rust! · 84937445
    SamwiseFilmore authored
    A naive basic upgrade implementation is done now. It iterates a lot and
    is currently requesting the entries from the new package once more than
    it needs to (Transaction::install is used directly after and calls
    read_entries() again). However, it does appear to work, as...
    
    I wrote some tests in rust! The API now has a test case running on it,
    so I'll know if I break API or some such. I designed it to be
    thread-safe, so parallel testing is possible (unique temp files for each
    thread, cleaned up automatically on drop).
    
    This commit also refactors the basic functions in the crate to use
    AsRef<Path> instead of &str, and reorganizes the imports in the crate to
    make everything look nicer in the rustdoc.
    84937445
  • SamwiseFilmore's avatar
    Refactor copy_and_hash · 1a9d144c
    SamwiseFilmore authored
    This needs some further changes to make the generic stuff a bit more
    sane and actually usable, but this ensures that there's no code
    duplication in this function.
    
    Involved changes to the PackageSrcExt trait to better provide a
    reading interface for entries: should be helpful in future
    1a9d144c
  • SamwiseFilmore's avatar
    Simplify copy_and_hash to take any writer; · 53bd3d56
    SamwiseFilmore authored
    Hashing a file should just pass it io::sink() and that'll probably be
    optimized away at compile. Significantly simplifies every call site too.
    53bd3d56
  • SamwiseFilmore's avatar
    Major Error Refactor · 0e2d0cae
    SamwiseFilmore authored
    This does a similar thing as pkgar_keys, by splitting up the errors into
    ErrorKind (which stores the immediate context of the error, for example,
    the entry name) and the real Error, which _can_ also have a reason or
    filename associated. Error includes a builder pattern for adding context
    which can be shorter and is obviously more elegant than what I was doing
    before.
    
    Error is also a struct with a private enum member, this is to keep users
    from interacting with the error's state, all of which should be accessible
    using std::error::Error.
    0e2d0cae
  • SamwiseFilmore's avatar
    Fix typing for crate::ext; Rename upgrade to replace · 53990857
    SamwiseFilmore authored
    PackageSrcExt will probably change, but the typing should be
    significantly more lenient
    53990857
  • SamwiseFilmore's avatar
    Refactoring Transaction; Docs · 802dc824
    SamwiseFilmore authored
    Trying to leverage the type system a bit more for Entry and Header
    attributes and refactor transaction code to make it more readable and
    concise. More coming here.
    802dc824
  • SamwiseFilmore's avatar
    Use bitflags! for mode defines · c0ff5505
    SamwiseFilmore authored
    I think this cleans up the code for matching on modes a little bit. At
    least most of it is now type checked so it'll be harder to break stuff
    with the mode defines.
    c0ff5505
  • SamwiseFilmore's avatar
    Refactor transactions · 1151de44
    SamwiseFilmore authored
    Instead of creating one transaction for all operations and calling a
    method on that transaction to add installs or removes, the operation
    methods are constructors for transactions, and a transaction represents
    the temprorary state on the filesystem of an operation on an individual
    package or pair of packages.
    
    Probably some convenience methods should be implemented for
    Vec<Transaction> for easy comitting, but the idea is to expose the
    stateful nature of the transactions to library users so that they can do
    all the expensive parts of a large operation and commit all package
    changes at once.
    1151de44
  • SamwiseFilmore's avatar
    Implement bin::verify · ee8a482b
    SamwiseFilmore authored
    Convenience function
    ee8a482b
  • SamwiseFilmore's avatar
    2312d23b
  • Jeremy Soller's avatar
    Merge branch 'transaction' into 'master' · d8de5f9f
    Jeremy Soller authored
    Transactions
    
    See merge request !9
    d8de5f9f
  • SamwiseFilmore's avatar
    Use error-chain for error handling · 0723ce95
    SamwiseFilmore authored
    There are still a couple of pain points here (EntryReader, for example),
    but the general layout works pretty well. With error chain and one or
    two handwritten impls I can pass a bare &Path to chain_err and it will
    construct the correct error variant and add it to the chain. This is a
    significant reduction in boilerplate.
    
    I still need to do some manual testing to generate some error
    conditions, but I think this setup will be more robust in the long term.
    0723ce95
  • SamwiseFilmore's avatar
    Error cleanup · 9caccb76
    SamwiseFilmore authored
    Most of this is just allowing "implicit" conversions between
    Path/PathBuf and ErrorKind, enabling `.chain_err(|| path )` syntax as
    opposed to `.chain_err(|| ErrorKind::Path(path.to_path_buf()) )`.
    Reduced verbosity should help with readability, since that pattern is
    used throughout the codebase.
    
    Also took care of a couple TODOs
    9caccb76
  • Jeremy Soller's avatar
    Merge branch 'error_chain' into 'master' · dbfc5cdb
    Jeremy Soller authored
    Error chain
    
    See merge request !10
    dbfc5cdb
  • Jeremy Soller's avatar
    Fix for compilation with stable rust · f5cbcc6a
    Jeremy Soller authored
    f5cbcc6a
  • Jeremy Soller's avatar
    98d0bbf5
  • Jeremy Soller's avatar
    0.1.7 - add pkgar split command · d0f5f6ec
    Jeremy Soller authored
    d0f5f6ec
  • Jeremy Soller's avatar
    Add version specifiers · 438a0514
    Jeremy Soller authored
    438a0514
  • Jeremy Soller's avatar
    Remove pkgar-keys tags · 2bf58075
    Jeremy Soller authored
    2bf58075
Showing
with 1435 additions and 120 deletions
[package]
name = "pkgar"
version = "0.1.6"
description = "Redox Package Archive"
license = "MIT"
authors = ["Jeremy Soller <jackpot51@gmail.com>"]
repository = "https://gitlab.redox-os.org/redox-os/pkgar"
edition = "2018"
[workspace]
members = [
"pkgar",
"pkgar-core",
"pkgar-keys",
]
[[bin]]
name = "pkgar"
required-features = ["clap", "rand", "std"]
[dependencies]
plain = "0.2.3"
sodalite = "0.3.0"
[dependencies.clap]
version = "2.33.0"
optional = true
[dependencies.rand]
version = "0.7.2"
optional = true
[dependencies.rand_core]
version = "0.5.1"
default-features = false
[dependencies.blake3]
version = "0.2.1"
default-features = false
features = ["rayon"]
[features]
default = ["clap", "rand", "std"]
std = []
# pkgar - Package Archive
pkgar refers to three related items - the file format, the library, and the
command line executable.
Pkgar is the package archive format for Redox OS.
The pkgar format is not designed to be the best format for all archive uses,
only the best default format for packages on Redox OS. It is reproducible,
meaning archiving a directory will produce the same results every time. It
provides cryptographic signatures and integrity checking for package files. It
also allows this functionality to be used without storing the entire package
archive, by only storing the package header. Large files, compression,
encryption, and random access are not optimized for. Little endian is currently
assumed, as well as Unix mode flags.
## Project Layout
There are currently two crates in this repo. See their READMEs for more specific
docs:
- `pkgar`: The implementation of the pkgar file format as a library, and a cli
tool for manpulating pkgar packages.
- `pkgar-keys`: Key management tool/library for pkgar.
***This specification is currently a work in progress***
## File Format - .pkgar
pkgar is a format for packages may be delivered in a single file (.pkgar), or as
a header file (.pkgar_head) with an associated data file (.pkgar_data). The
purpose of this is to allow downloading a header only and verifying local files
before downloading file data. Concatenating the header and data files creates a
valid single file: `cat example.pkgar_head example.pkgar_data > example.pkgar`
### Header Portion
The header portion is designed to contain the data required to verify files
already installed on disk. It is signed using NaCl (or a compatible
implementation such as libsodium), and contains the blake3, offset, size, mode,
and name of each file. The user and group IDs are left out intentionally, to
support the installation of a package either as root or as a user, for example,
in the user's home directory.
#### Header Struct
The size of the header struct is 136 bytes. All fields are packed.
- signature - 512-bit (64 byte) NaCl signature of header data
- public_key - 256-bit (32 byte) NaCl public key used to generate signature
- blake3 - 256-bit (32 byte) blake3 sum of the entry data
- count - 64-bit count of entry structs, which immediately follow
#### Entry Struct
The size of the entry struct is 308 bytes. All fields are packed.
- blake3 - 256-bit (32 byte) blake3 sum of the file data
- offset - 64-bit little endian offset of file data in the data portion
- size - 64-bit little endian size in bytes of the file data in the data portion
- mode - 32-bit Unix permissions (user, group, other with read, write, execute)
- path - 256 byte NUL-terminated relative path from extract directory
### Data Portion
The data portion is used to look up file data only. It could be compressed to
produce a .pkgar_data.gz file, for example. It can be removed after the install
is completed. It is possible for it to contain holes, invalid data, or
unreferenced data - so long as the blake3 of files identified in the header are
still valid. This data should be removed when an archive is rebuilt.
### Operation
A reader should first verify the header portion's signature matches that of a
valid package source. Then, they should locate the entry for the file of
interest. If desired, they can check if a locally cached file matches the
referenced blake3. If this is not the case, they may access the data portion and
verify that the data at the offset and length in the header entry matches the
blake3. In that case, the data may be retrieved.
[package]
name = "pkgar-core"
version = "0.1.0"
description = "Core Data Types for the Redox Package Archive"
license = "MIT"
authors = ["Jeremy Soller <jackpot51@gmail.com>", "Wesley Hershberger <mggmugginsmc@gmail.com>"]
repository = "https://gitlab.redox-os.org/redox-os/pkgar"
edition = "2018"
[dependencies]
bitflags = "1.2.1"
blake3 = { version = "0.3.6", default_features = false, features = ["rayon"] }
plain = "0.2.3"
sodiumoxide = { version = "=0.2.6", default_features = false }
//! The packed structs represent the on-disk format of pkgar
use blake3::Hash;
use plain::Plain;
#[derive(Clone, Copy)]
use crate::{Error, Mode};
#[derive(Clone, Copy, Debug)]
#[repr(packed)]
pub struct Entry {
/// Blake3 sum of the file data
......@@ -18,8 +20,25 @@ pub struct Entry {
}
impl Entry {
pub fn blake3(&self) -> Hash {
Hash::from(self.blake3)
}
pub fn offset(&self) -> u64 {
self.offset
}
pub fn size(&self) -> u64 {
self.size
}
pub fn mode(&self) -> Result<Mode, Error> {
Mode::from_bits(self.mode)
.ok_or(Error::InvalidMode(self.mode))
}
/// Retrieve the path, ending at the first NUL
pub fn path(&self) -> &[u8] {
pub fn path_bytes(&self) -> &[u8] {
let mut i = 0;
while i < self.path.len() {
if self.path[i] == 0 {
......@@ -32,3 +51,4 @@ impl Entry {
}
unsafe impl Plain for Entry {}
use alloc::format;
use alloc::string::ToString;
use core::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
InvalidBlake3,
InvalidData,
InvalidKey,
InvalidMode(u32),
InvalidSignature,
Plain(plain::Error),
Overflow,
TryFromInt(core::num::TryFromIntError),
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
use Error::*;
let msg = match self {
InvalidBlake3 => "Invalid Blake3".to_string(),
InvalidData => "Data Invalid".to_string(),
InvalidKey => "Key Invalid".to_string(),
InvalidMode(mode) => format!("Invalid Mode: {:o}", mode),
InvalidSignature => "Invalid Signature".to_string(),
Plain(err) => format!("Plain: {:?}", err),
Overflow => "Overflow".to_string(),
TryFromInt(err) => format!("TryFromInt: {}", err),
};
write!(f, "{}", msg)
}
}
impl From<plain::Error> for Error {
fn from(err: plain::Error) -> Error {
Error::Plain(err)
}
}
impl From<core::num::TryFromIntError> for Error {
fn from(err: core::num::TryFromIntError) -> Error {
Error::TryFromInt(err)
}
}
......@@ -3,10 +3,11 @@
use core::convert::TryFrom;
use core::mem;
use plain::Plain;
use sodiumoxide::crypto::sign::{self, PublicKey};
use crate::{Entry, Error, PublicKey};
use crate::{Entry, Error};
#[derive(Clone, Copy)]
#[derive(Clone, Copy, Debug)]
#[repr(packed)]
pub struct Header {
/// NaCl signature of header data
......@@ -28,19 +29,17 @@ impl Header {
let signed = data.get(..mem::size_of::<Header>())
.ok_or(Error::Plain(plain::Error::TooShort))?;
// Verify signature and retrieve verified data
let mut verified = [0; mem::size_of::<Header>()];
let count = sodalite::sign_attached_open(&mut verified, signed, public_key.as_data())
let verified = sign::verify(signed, public_key)
.map_err(|_err| Error::InvalidSignature)?;
// Check that verified data matches signed data after skipping the signature
if &verified[..count] != &signed[64..] {
if verified.as_slice() != &signed[64..] {
return Err(Error::InvalidData);
}
// Create header from signed data and check that public key matches
let header: &Header = unsafe { Header::new_unchecked(signed)? };
if &header.public_key != &public_key.as_data()[..] {
if &header.public_key != &public_key.as_ref()[..] {
return Err(Error::InvalidKey);
}
......@@ -49,14 +48,16 @@ impl Header {
/// Parse header from raw header data without verification
pub unsafe fn new_unchecked<'a>(data: &'a [u8]) -> Result<&'a Header, Error> {
plain::from_bytes(data)
.map_err(Error::Plain)
Ok(plain::from_bytes(data)?)
}
pub fn count(&self) -> u64 {
self.count
}
/// Retrieve the size of the entries
pub fn entries_size(&self) -> Result<u64, Error> {
let entry_size = u64::try_from(mem::size_of::<Entry>())
.map_err(Error::TryFromInt)?;
let entry_size = u64::try_from(mem::size_of::<Entry>())?;
self.count
.checked_mul(entry_size)
.ok_or(Error::Overflow)
......@@ -64,8 +65,7 @@ impl Header {
/// Retrieve the size of the Header and its entries
pub fn total_size(&self) -> Result<u64, Error> {
let header_size = u64::try_from(mem::size_of::<Header>())
.map_err(Error::TryFromInt)?;
let header_size = u64::try_from(mem::size_of::<Header>())?;
self.entries_size()?
.checked_add(header_size)
.ok_or(Error::Overflow)
......@@ -73,8 +73,7 @@ impl Header {
/// Parse entries from raw entries data and verify using blake3
pub fn entries<'a>(&self, data: &'a [u8]) -> Result<&'a [Entry], Error> {
let entries_size = usize::try_from(self.entries_size()?)
.map_err(Error::TryFromInt)?;
let entries_size = usize::try_from(self.entries_size()?)?;
let entries_data = data.get(..entries_size)
.ok_or(Error::Plain(plain::Error::TooShort))?;
......@@ -95,7 +94,18 @@ impl Header {
/// Parse entries from raw entries data without verification
pub unsafe fn entries_unchecked<'a>(data: &'a [u8]) -> Result<&'a [Entry], Error> {
plain::slice_from_bytes(data)
.map_err(Error::Plain)
Ok(plain::slice_from_bytes(data)?)
}
}
/*
impl fmt::Debug for Header {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Header {{\n\tsignature: {:?},\n\tpublic_key: {:?},\n\tblake3: {:?},count: {:?},\n}}",
&self.signature[..],
self.public_key,
self.blake3,
self.count(),
)
}
}*/
#![cfg_attr(not(feature = "std"), no_std)]
#![no_std]
extern crate alloc;
use core::mem;
use bitflags::bitflags;
pub use crate::entry::Entry;
pub use crate::error::Error;
pub use crate::header::Header;
pub use crate::key::{PublicKey, SecretKey};
pub use crate::package::{Package, PackageSrc};
pub use crate::package::{PackageBuf, PackageSrc};
mod entry;
mod error;
mod header;
mod key;
mod package;
#[cfg(feature = "std")]
pub mod bin;
pub const HEADER_SIZE: usize = mem::size_of::<Header>();
pub const ENTRY_SIZE: usize = mem::size_of::<Entry>();
bitflags! {
/// Ensures that all platforms use the same mode defines.
pub struct Mode: u32 {
const PERM = 0o007777;
const KIND = 0o170000;
const FILE = 0o100000;
const SYMLINK = 0o120000;
}
}
impl Mode {
/// Only any kind bits
pub fn kind(self) -> Mode {
self & Mode::KIND
}
/// Only any permissions bits
pub fn perm(self) -> Mode {
self & Mode::PERM
}
}
#[cfg(test)]
mod tests {
use core::mem;
use crate::{Entry, Header};
use crate::{Entry, ENTRY_SIZE, Header, HEADER_SIZE};
#[test]
fn header_size() {
assert_eq!(mem::size_of::<Header>(), 136);
assert_eq!(HEADER_SIZE, 136);
}
#[test]
fn entry_size() {
assert_eq!(mem::size_of::<Entry>(), 308);
assert_eq!(ENTRY_SIZE, 308);
}
}
use alloc::vec;
use alloc::vec::Vec;
use core::convert::TryFrom;
use sodiumoxide::crypto::sign::PublicKey;
use crate::{Entry, Error, HEADER_SIZE, Header};
pub trait PackageSrc {
type Err: From<Error>;
fn read_at(&mut self, offset: u64, buf: &mut [u8]) -> Result<usize, Self::Err>;
fn header(&self) -> Header;
/// Users of implementors of `PackageSrc` should use `header` instead of `read_header` for
/// cheap header access.
/// Implementors of `PackageSrc` should call this function during initialization and store
/// the result to pass out with `header`.
fn read_header(&mut self, public_key: &PublicKey) -> Result<Header, Self::Err> {
let mut header_data = [0; HEADER_SIZE];
self.read_at(0, &mut header_data)?;
let header = Header::new(&header_data, &public_key)?;
Ok(header.clone())
}
fn read_entries(&mut self) -> Result<Vec<Entry>, Self::Err> {
let header = self.header();
let entries_size = header.entries_size()
.and_then(|rslt| usize::try_from(rslt)
.map_err(Error::TryFromInt)
)?;
let mut entries_data = vec![0; entries_size];
self.read_at(HEADER_SIZE as u64, &mut entries_data)?;
let entries = header.entries(&entries_data)?;
Ok(entries.to_vec())
}
/// Read from this src at a given entry's data with a given offset within that entry
fn read_entry(&mut self, entry: Entry, offset: usize, buf: &mut [u8]) -> Result<usize, Self::Err> {
if offset as u64 > entry.size {
return Ok(0);
}
let mut end = usize::try_from(entry.size - offset as u64)
.map_err(Error::TryFromInt)?;
if end > buf.len() {
end = buf.len();
}
let offset =
HEADER_SIZE as u64 +
self.header().entries_size()? +
entry.offset + offset as u64;
self.read_at(offset as u64, &mut buf[..end])
}
}
//TODO: Test this impl...
pub struct PackageBuf<'a> {
src: &'a [u8],
header: Header,
}
impl<'a> PackageBuf<'a> {
pub fn new(src: &'a [u8], public_key: &PublicKey) -> Result<PackageBuf<'a>, Error> {
let zeroes = [0; HEADER_SIZE];
let mut new = PackageBuf {
src,
header: unsafe { *Header::new_unchecked(&zeroes)? },
};
new.header = *Header::new(&new.src, &public_key)?;
Ok(new)
}
}
impl PackageSrc for PackageBuf<'_> {
type Err = Error;
fn header(&self) -> Header {
self.header
}
fn read_at(&mut self, offset: u64, buf: &mut [u8]) -> Result<usize, Error> {
let start = usize::try_from(offset)
.map_err(Error::TryFromInt)?;
let len = self.src.len();
if start >= len {
return Ok(0);
}
let mut end = start.checked_add(buf.len())
.ok_or(Error::Overflow)?;
if end > len {
end = len;
}
buf.copy_from_slice(&self.src[start..end]);
Ok(end.checked_sub(start).unwrap())
}
}
[package]
name = "pkgar-keys"
version = "0.1.0"
description = "Key management tool/library for pkgar"
license = "MIT"
authors = ["Wesley Hershberger <mggmugginsmc@gmail.com>"]
repository = "https://gitlab.redox-os.org/redox-os/pkgar"
readme = "README.md"
edition = "2018"
[dependencies]
clap = "2.33.3"
dirs = "3.0.1"
error-chain = "0.12"
hex = { version = "0.4.2", features = ["serde"] }
lazy_static = "1.4.0"
seckey = "0.11.2"
serde = { version = "1.0.115", default_features = false, features = ["derive"] }
sodiumoxide = { version = "=0.2.6", default_features = false }
termion = "1.5.5"
#thiserror = "1.0.20"
toml = "0.5.6"
user-error = "1.2.8"
# `pkgar-keys`
Key management tool/library for pkgar.
Run the binary with `--help` for CLI documentation. Keys are stored in
`~/.pkgar/keys` by default and are stored as toml. The key format is subject
to change.
use std::io;
use std::path::{Path, PathBuf};
use error_chain::error_chain;
use user_error::UFE;
//use thiserror::Error;
error_chain! {
types {
Error, ErrorKind, ResultExt;
}
foreign_links {
Io(io::Error);
Ser(toml::ser::Error);
Deser(toml::de::Error);
}
errors {
KeyInvalid {
description("Key length invalid"),
}
KeyMismatch {
description("Public and secret keys do not match"),
}
NonceInvalid {
description("Invalid nonce length"),
}
PassphraseIncorrect {
description("Incorrect passphrase"),
}
PassphraseMismatch {
description("Passphrases did not match"),
}
Path(path: PathBuf) {
display("{}: ", path.display()),
}
}
skip_msg_variant
}
impl UFE for Error {}
// Allow .chain_err(|| path )
impl From<&Path> for ErrorKind {
fn from(path: &Path) -> ErrorKind {
ErrorKind::Path(path.to_path_buf())
}
}
impl From<&PathBuf> for ErrorKind {
fn from(path: &PathBuf) -> ErrorKind {
ErrorKind::Path(path.clone())
}
}
mod error;
use std::fs::{self, File, OpenOptions};
use std::io::{self, stdin, stdout, Write};
use std::ops::Deref;
use std::os::unix::fs::OpenOptionsExt;
use std::path::{Path, PathBuf};
use error_chain::bail;
use hex::FromHex;
use lazy_static::lazy_static;
use seckey::SecBytes;
use serde::{Deserialize, Serialize};
use sodiumoxide::crypto::{
pwhash,
secretbox,
sign,
};
use termion::input::TermRead;
pub use crate::error::{ErrorKind, Error, ResultExt};
lazy_static! {
static ref HOMEDIR: PathBuf = {
dirs::home_dir()
.unwrap_or("./".into())
};
/// The default location for pkgar to look for the user's public key.
///
/// Defaults to `$HOME/.pkgar/keys/id_ed25519.pub.toml`. If `$HOME` is
/// unset, `./.pkgar/keys/id_ed25519.pub.toml`.
pub static ref DEFAULT_PUBKEY: PathBuf = {
Path::join(&HOMEDIR, ".pkgar/keys/id_ed25519.pub.toml")
};
/// The default location for pkgar to look for the user's secret key.
///
/// Defaults to `$HOME/.pkgar/keys/id_ed25519.toml`. If `$HOME` is unset,
/// `./.pkgar/keys/id_ed25519.toml`.
pub static ref DEFAULT_SECKEY: PathBuf = {
Path::join(&HOMEDIR, ".pkgar/keys/id_ed25519.toml")
};
}
mod ser {
use hex::FromHex;
use serde::{Deserialize, Deserializer};
use serde::de::Error;
use sodiumoxide::crypto::{pwhash, secretbox, sign};
//TODO: Macro?
pub(crate) fn to_salt<'d, D: Deserializer<'d>>(deser: D) -> Result<pwhash::Salt, D::Error> {
String::deserialize(deser)
.and_then(|s| <[u8; 32]>::from_hex(s)
.map(|val| pwhash::Salt(val) )
.map_err(|err| Error::custom(err.to_string()) ) )
}
pub(crate) fn to_nonce<'d, D: Deserializer<'d>>(deser: D) -> Result<secretbox::Nonce, D::Error> {
String::deserialize(deser)
.and_then(|s| <[u8; 24]>::from_hex(s)
.map(|val| secretbox::Nonce(val) )
.map_err(|err| Error::custom(err.to_string()) ) )
}
pub(crate) fn to_pubkey<'d, D: Deserializer<'d>>(deser: D) -> Result<sign::PublicKey, D::Error> {
String::deserialize(deser)
.and_then(|s| <[u8; 32]>::from_hex(s)
.map(|val| sign::PublicKey(val) )
.map_err(|err| Error::custom(err.to_string()) ) )
}
}
/// Standard pkgar public key format definition. Use serde to serialize/deserialize
/// files into this struct (helper methods available).
#[derive(Deserialize, Serialize)]
pub struct PublicKeyFile {
#[serde(serialize_with = "hex::serialize", deserialize_with = "ser::to_pubkey")]
pub pkey: sign::PublicKey,
}
impl PublicKeyFile {
/// Parse a `PublicKeyFile` from `file` (in toml format).
pub fn open(file: impl AsRef<Path>) -> Result<PublicKeyFile, Error> {
let content = fs::read_to_string(&file)
.chain_err(|| file.as_ref() )?;
toml::from_str(&content)
.chain_err(|| file.as_ref() )
}
/// Write `self` serialized as toml to `w`.
pub fn write(&self, mut w: impl Write) -> Result<(), Error> {
w.write_all(toml::to_string(self)?.as_bytes())?;
Ok(())
}
/// Shortcut to write the public key to `file`
pub fn save(&self, file: impl AsRef<Path>) -> Result<(), Error> {
self.write(
File::create(&file)
.chain_err(|| file.as_ref() )?
).chain_err(|| file.as_ref() )
}
}
enum SKey {
Cipher([u8; 80]),
Plain(sign::SecretKey),
}
impl SKey {
fn encrypt(&mut self, passwd: Passwd, salt: pwhash::Salt, nonce: secretbox::Nonce) {
if let SKey::Plain(skey) = self {
if let Some(passwd_key) = passwd.gen_key(salt) {
let mut buf = [0; 80];
buf.copy_from_slice(&secretbox::seal(skey.as_ref(), &nonce, &passwd_key));
*self = SKey::Cipher(buf);
}
}
}
fn decrypt(&mut self, passwd: Passwd, salt: pwhash::Salt, nonce: secretbox::Nonce) -> Result<(), Error> {
if let SKey::Cipher(ciphertext) = self {
if let Some(passwd_key) = passwd.gen_key(salt) {
let skey_plain = secretbox::open(ciphertext.as_ref(), &nonce, &passwd_key)
.map_err(|_| ErrorKind::PassphraseIncorrect )?;
*self = SKey::Plain(sign::SecretKey::from_slice(&skey_plain)
.ok_or(ErrorKind::KeyInvalid)?);
} else {
*self = SKey::Plain(sign::SecretKey::from_slice(&ciphertext[..64])
.ok_or(ErrorKind::KeyInvalid)?);
}
}
Ok(())
}
/// Returns `None` if encrypted
fn skey(&self) -> Option<sign::SecretKey> {
match &self {
SKey::Plain(skey) => Some(skey.clone()),
SKey::Cipher(_) => None,
}
}
}
impl AsRef<[u8]> for SKey {
fn as_ref(&self) -> &[u8] {
match self {
SKey::Cipher(buf) => buf.as_ref(),
SKey::Plain(skey) => skey.as_ref(),
}
}
}
impl FromHex for SKey {
type Error = hex::FromHexError;
fn from_hex<T: AsRef<[u8]>>(buf: T) -> Result<SKey, hex::FromHexError> {
let bytes = hex::decode(buf)?;
// Public key is only 64 bytes...
if bytes.len() == 64 {
Ok(SKey::Plain(sign::SecretKey::from_slice(&bytes)
.expect("Somehow not the right number of bytes")))
} else {
let mut buf = [0; 80];
buf.copy_from_slice(&bytes);
Ok(SKey::Cipher(buf))
}
}
}
/// Standard pkgar private key format definition. Use serde.
/// Internally, this struct stores the encrypted state of the private key as an enum.
/// Manipulate the state using the `encrypt()`, `decrypt()` and `is_encrypted()`.
#[derive(Deserialize, Serialize)]
pub struct SecretKeyFile {
#[serde(serialize_with = "hex::serialize", deserialize_with = "ser::to_salt")]
salt: pwhash::Salt,
#[serde(serialize_with = "hex::serialize", deserialize_with = "ser::to_nonce")]
nonce: secretbox::Nonce,
#[serde(with = "hex")]
skey: SKey,
}
impl SecretKeyFile {
/// Generate a keypair with all the nessesary info to save both keys. You
/// must call `save()` on each object to persist them to disk.
pub fn new() -> (PublicKeyFile, SecretKeyFile) {
let (pkey, skey) = sign::gen_keypair();
let pkey_file = PublicKeyFile { pkey };
let skey_file = SecretKeyFile {
salt: pwhash::gen_salt(),
nonce: secretbox::gen_nonce(),
skey: SKey::Plain(skey),
};
(pkey_file, skey_file)
}
/// Parse a `SecretKeyFile` from `file` (in toml format).
pub fn open(file: impl AsRef<Path>) -> Result<SecretKeyFile, Error> {
let content = fs::read_to_string(&file)
.chain_err(|| file.as_ref() )?;
toml::from_str(&content)
.chain_err(|| file.as_ref() )
}
/// Write `self` serialized as toml to `w`.
pub fn write(&self, mut w: impl Write) -> Result<(), Error> {
w.write_all(toml::to_string(&self)?.as_bytes())?;
Ok(())
}
/// Shortcut to write the secret key to `file`.
///
/// Make sure to call `encrypt()` in order to encrypt
/// the private key, otherwise it will be stored as plain text.
pub fn save(&self, file: impl AsRef<Path>) -> Result<(), Error> {
self.write(
OpenOptions::new()
.write(true)
.create(true)
.mode(0o600)
.open(&file)
.chain_err(|| file.as_ref() )?
).chain_err(|| file.as_ref() )
}
/// Ensure that the internal state of this struct is encrypted.
/// Note that if passwd is empty, this function is a no-op.
pub fn encrypt(&mut self, passwd: Passwd) {
self.skey.encrypt(passwd, self.salt, self.nonce)
}
/// Ensure that the internal state of this struct is decrypted.
/// If the internal state is already decrypted, this function is a no-op.
pub fn decrypt(&mut self, passwd: Passwd) -> Result<(), Error> {
self.skey.decrypt(passwd, self.salt, self.nonce)
}
/// Status of the internal state.
pub fn is_encrypted(&self) -> bool {
match self.skey {
SKey::Cipher(_) => true,
SKey::Plain(_) => false,
}
}
/// Returns `None` if the secret key is encrypted.
pub fn key(&mut self) -> Option<sign::SecretKey> {
match &self.skey {
SKey::Plain(skey) => Some(skey.clone()),
SKey::Cipher(_) => None,
}
}
/// Returns `None` if the secret key is encrypted.
pub fn public_key_file(&self) -> Option<PublicKeyFile> {
Some(PublicKeyFile {
pkey: self.skey.skey()?.public_key(),
})
}
}
/// Secure in-memory representation of a password.
pub struct Passwd {
bytes: SecBytes,
}
impl Passwd {
/// Create a new `Passwd` and zero the old string.
pub fn new(passwd: &mut String) -> Passwd {
let pwd = Passwd {
bytes :SecBytes::with(
passwd.len(),
|buf| buf.copy_from_slice(passwd.as_bytes())
),
};
unsafe {
seckey::zero(passwd.as_bytes_mut());
}
pwd
}
/// Prompt the user for a `Passwd` on stdin.
pub fn prompt(prompt: impl AsRef<str>) -> Result<Passwd, Error> {
let stdout = stdout();
let mut stdout = stdout.lock();
let stdin = stdin();
let mut stdin = stdin.lock();
stdout.write_all(prompt.as_ref().as_bytes())?;
stdout.flush()?;
let mut passwd = stdin.read_passwd(&mut stdout)?
.ok_or(ErrorKind::Io(
io::Error::new(
io::ErrorKind::UnexpectedEof,
"Invalid Password Input",
)
))?;
println!();
Ok(Passwd::new(&mut passwd))
}
/// Prompt for a password on stdin and confirm it. For configurable
/// prompts, use [`Passwd::prompt`](struct.Passwd.html#method.prompt).
pub fn prompt_new() -> Result<Passwd, Error> {
let passwd = Passwd::prompt(
"Please enter a new passphrase (leave empty to store the key in plaintext): "
)?;
let confirm = Passwd::prompt("Please re-enter the passphrase: ")?;
if passwd != confirm {
bail!(ErrorKind::PassphraseMismatch);
}
Ok(passwd)
}
/// Get a key for symmetric key encryption from a password.
fn gen_key(&self, salt: pwhash::Salt) -> Option<secretbox::Key> {
if self.bytes.read().len() > 0 {
let mut key = secretbox::Key([0; secretbox::KEYBYTES]);
let secretbox::Key(ref mut binary_key) = key;
pwhash::derive_key(
binary_key,
&self.bytes.read(),
&salt,
pwhash::OPSLIMIT_INTERACTIVE,
pwhash::MEMLIMIT_INTERACTIVE,
).expect("Failed to get key from password");
Some(key)
} else {
None
}
}
}
impl PartialEq for Passwd {
fn eq(&self, other: &Passwd) -> bool {
self.bytes.read().deref() == other.bytes.read().deref()
}
}
impl Eq for Passwd {}
/// Generate a new keypair. The new keys will be saved to `file`. The user
/// will be prompted on stdin for a password, empty passwords will cause the
/// secret key to be stored in plain text. Note that parent
/// directories will not be created.
pub fn gen_keypair(pkey_path: &Path, skey_path: &Path) -> Result<(PublicKeyFile, SecretKeyFile), Error> {
let passwd = Passwd::prompt_new()
.chain_err(|| skey_path )?;
let (pkey_file, mut skey_file) = SecretKeyFile::new();
skey_file.encrypt(passwd);
skey_file.save(skey_path)?;
pkey_file.save(pkey_path)?;
println!("Generated {} and {}", pkey_path.display(), skey_path.display());
Ok((pkey_file, skey_file))
}
fn prompt_skey(skey_path: &Path, prompt: impl AsRef<str>) -> Result<SecretKeyFile, Error> {
let mut key_file = SecretKeyFile::open(skey_path)?;
if key_file.is_encrypted() {
let passwd = Passwd::prompt(&format!("{} {}: ", prompt.as_ref(), skey_path.display()))
.chain_err(|| skey_path )?;
key_file.decrypt(passwd)
.chain_err(|| skey_path )?;
}
Ok(key_file)
}
/// Get a SecretKeyFile from a path. If the file is encrypted, prompt for a password on stdin.
pub fn get_skey(skey_path: &Path) -> Result<SecretKeyFile, Error> {
prompt_skey(skey_path, "Passphrase for")
}
/// Open, decrypt, re-encrypt with a different passphrase from stdin, and save the newly encrypted
/// secret key at `skey_path`.
pub fn re_encrypt(skey_path: &Path) -> Result<(), Error> {
let mut skey_file = prompt_skey(skey_path, "Old passphrase for")?;
let passwd = Passwd::prompt_new()
.chain_err(|| skey_path )?;
skey_file.encrypt(passwd);
skey_file.save(skey_path)
}
use std::io;
use std::fs;
use std::path::{Path, PathBuf};
use std::process;
use clap::clap_app;
use user_error::UFE;
use pkgar_keys::{
DEFAULT_PUBKEY,
DEFAULT_SECKEY,
Error,
ErrorKind,
gen_keypair,
get_skey,
ResultExt,
SecretKeyFile,
re_encrypt
};
fn cli() -> Result<i32, Error> {
let matches = clap_app!(("pkgar-keys") =>
(author: "Wesley Hershberger <mggmugginsmc@gmail.com>")
(about: "NaCl key management for pkgar")
(@arg skey: -s --skey [FILE] +global "Alternate secret keyfile (defaults to '~/.pkgar/keys/id_ed25519.toml')")
(@setting SubcommandRequired)
(@subcommand gen =>
(about: "Generate a keypair and store on the filesystem")
(@arg pkey: -p --pkey [FILE]
"Alternate public keyfile (defaults to `~/.pkgar/keys/id_ed25519.pub.toml`)")
(@arg plaintext: -P --plaintext
"Do not prompt for a passphrase and store the secret key as plain text")
(@arg force: -f --force
"Don't check for existing files before generating a new keypair")
)
(@subcommand rencrypt =>
(about: "Re-encrypt the secret key provided by --skey")
)
(@subcommand export =>
(about: "Print the public key corresponding to the key given with --skey in the pkgar pubkey format")
(@arg file: -f --file [FILE] "Output to a file instead of stdout")
)
).get_matches();
let skey_path = matches.value_of("skey")
.map(|file| PathBuf::from(file) )
.unwrap_or(DEFAULT_SECKEY.clone());
let (subcommand, submatches) = matches.subcommand();
let submatches = submatches
.expect("A subcommand should have been provided");
match subcommand {
"gen" => {
if let Some(keydir) = skey_path.parent() {
fs::create_dir_all(&keydir)
.chain_err(|| keydir )?;
}
if ! submatches.is_present("force") {
if skey_path.exists() {
return Err(Error::from_kind(ErrorKind::Io(
io::Error::from(io::ErrorKind::AlreadyExists)
)))
.chain_err(|| &skey_path );
}
}
let pkey_path = submatches.value_of("pkey")
.map(|file| PathBuf::from(file) )
.unwrap_or(DEFAULT_PUBKEY.clone());
if ! submatches.is_present("plaintext") {
gen_keypair(&pkey_path, &skey_path)?;
} else {
let (pkey, skey) = SecretKeyFile::new();
pkey.save(&pkey_path)?;
skey.save(&skey_path)?;
}
},
"export" => {
let skey = get_skey(&skey_path)?;
let pkey = skey.public_key_file()
.expect("Secret key was encrypted after being decrypted");
if let Some(file) = submatches.value_of("file") {
pkey.save(file)?;
} else {
pkey.write(io::stdout().lock())
.chain_err(|| Path::new("stdout") )?;
}
},
"rencrypt" => {
re_encrypt(&skey_path)?;
println!("Successfully re-encrypted {}", skey_path.display());
},
_ => unreachable!(),
}
Ok(0)
}
fn main() {
let code = cli().unwrap_or_else(|err| {
eprintln!("{}", err.into_ufe());
process::exit(1);
});
process::exit(code);
}
[package]
name = "pkgar"
version = "0.1.7"
description = "Redox Package Archive"
license = "MIT"
authors = ["Jeremy Soller <jackpot51@gmail.com>", "Wesley Hershberger <mggmugginsmc@gmail.com>"]
repository = "https://gitlab.redox-os.org/redox-os/pkgar"
edition = "2018"
[dependencies]
error-chain = "0.12"
plain = "0.2.3"
pkgar-core = { version = "0.1.0", path = "../pkgar-core" }
pkgar-keys = { version = "0.1.0", path = "../pkgar-keys" }
sodiumoxide = { version = "=0.2.6", default_features = false }
#thiserror = "1.0.20"
user-error = "1.2.8"
[dependencies.clap]
version = "2.33.3"
optional = true
[dependencies.blake3]
version = "0.3.6"
default-features = false
features = ["rayon"]
[dev-dependencies]
copy_dir = "0.1.2"
tempfile = "3.1.0"
[features]
default = ["clap", "std"]
std = []
# pkgar - Package Archive
pkgar refers to three related items - the file format, the library, and the
command line executable.
The pkgar format is not designed to be the best format for all archive uses,
only the best default format for packages on Redox OS. It is reproducible,
meaning archiving a directory will produce the same results every time. It
provides cryptographic signatures and integrity checking for package files. It
also allows this functionality to be used without storing the entire package
archive, by only storing the package header. Large files, compression,
encryption, and random access are not optimized for. Little endian is currently
assumed, as well as Unix mode flags.
***This specification is currently a work in progress***
## File Format - .pkgar
pkgar is a format for packages may be delivered in a single file (.pkgar), or as
a header file (.pkgar_head) with an associated data file (.pkgar_data). The
purpose of this is to allow downloading a header only and verifying local files
before downloading file data. Concatenating the header and data files creates a
valid single file: `cat example.pkgar_head example.pkgar_data > example.pkgar`
### Header Portion
The header portion is designed to contain the data required to verify files
already installed on disk. It is signed using NaCl (or a compatible
implementation such as libsodium), and contains the blake3, offset, size, mode,
and name of each file. The user and group IDs are left out intentionally, to
support the installation of a package either as root or as a user, for example,
in the user's home directory.
#### Header Struct
The size of the header struct is 136 bytes. All fields are packed.
- signature - 512-bit (64 byte) NaCl signature of header data
- public_key - 256-bit (32 byte) NaCl public key used to generate signature
- blake3 - 256-bit (32 byte) blake3 sum of the entry data
- count - 64-bit count of entry structs, which immediately follow
#### Entry Struct
The size of the entry struct is 308 bytes. All fields are packed.
- blake3 - 256-bit (32 byte) blake3 sum of the file data
- offset - 64-bit little endian offset of file data in the data portion
- size - 64-bit little endian size in bytes of the file data in the data portion
- mode - 32-bit Unix permissions (user, group, other with read, write, execute)
- path - 256 byte NUL-terminated relative path from extract directory
### Data Portion
The data portion is used to look up file data only. It could be compressed to
produce a .pkgar_data.gz file, for example. It can be removed after the install
is completed. It is possible for it to contain holes, invalid data, or
unreferenced data - so long as the blake3 of files identified in the header are
still valid. This data should be removed when an archive is rebuilt.
### Operation
A reader should first verify the header portion's signature matches that of a
valid package source. Then, they should locate the entry for the file of
interest. If desired, they can check if a locally cached file matches the
referenced blake3. If this is not the case, they may access the data portion and
verify that the data at the offset and length in the header entry matches the
blake3. In that case, the data may be retrieved.
## Development
To run the integration tests, you'll need to have pkgar-keys in your $PATH (or the
$PATH of the test script). Clone the repo from
[https://gitlab.redox-os.org/MggMuggins/pkgar-keys]() and run `cargo install --path .`.
Use `test.sh` to run the integration tests.
File moved
File moved
use blake3::Hash;
use std::ffi::OsStr;
use std::fs;
use std::fs::{self, File};
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::{symlink, OpenOptionsExt, PermissionsExt};
use std::path::{Component, Path};
use crate::{Entry, Error, Header, Package, PackageSrc, PublicKey, SecretKey};
// This ensures that all platforms use the same mode defines
const MODE_PERM: u32 = 0o7777;
const MODE_KIND: u32 = 0o170000;
const MODE_FILE: u32 = 0o100000;
const MODE_SYMLINK: u32 = 0o120000;
fn copy_hash<R: Read, W: Write>(mut read: R, mut write: W, buf: &mut [u8]) -> Result<(u64, Hash), Error> {
let mut hasher = blake3::Hasher::new();
let mut total = 0;
loop {
let count = read.read(buf)
.map_err(Error::Io)?;
if count == 0 {
break;
}
total += count as u64;
//TODO: Progress
write.write_all(&buf[..count])
.map_err(Error::Io)?;
hasher.update_with_join::<blake3::join::RayonJoin>(&buf[..count]);
}
Ok((total, hasher.finalize()))
}
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use pkgar_core::{Entry, Header, Mode, PackageSrc};
use pkgar_keys::PublicKeyFile;
use sodiumoxide::crypto::sign;
use crate::{Error, ErrorKind, READ_WRITE_HASH_BUF_SIZE, ResultExt};
use crate::ext::{copy_and_hash, EntryExt};
use crate::package::PackageFile;
use crate::transaction::Transaction;
fn folder_entries<P, Q>(base: P, path: Q, entries: &mut Vec<Entry>) -> io::Result<()>
where P: AsRef<Path>, Q: AsRef<Path>
......@@ -69,11 +50,15 @@ fn folder_entries<P, Q>(base: P, path: Q, entries: &mut Vec<Entry>) -> io::Resul
path_bytes[..relative_bytes.len()].copy_from_slice(relative_bytes);
let file_type = metadata.file_type();
let mut mode = metadata.permissions().mode() & MODE_PERM;
let file_mode = metadata.permissions().mode();
//TODO: Use pkgar_core::Mode for all ops. This is waiting on error
// handling.
let mut mode = file_mode & Mode::PERM.bits();
if file_type.is_file() {
mode |= MODE_FILE;
mode |= Mode::FILE.bits();
} else if file_type.is_symlink() {
mode |= MODE_SYMLINK;
mode |= Mode::SYMLINK.bits();
} else {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
......@@ -93,17 +78,14 @@ fn folder_entries<P, Q>(base: P, path: Q, entries: &mut Vec<Entry>) -> io::Resul
Ok(())
}
pub fn create(secret_path: &str, archive_path: &str, folder: &str) -> Result<(), Error> {
let secret_key = {
let mut data = [0; 64];
fs::OpenOptions::new()
.read(true)
.open(secret_path)
.map_err(Error::Io)?
.read_exact(&mut data)
.map_err(Error::Io)?;
SecretKey::from_data(data)
};
pub fn create(
secret_path: impl AsRef<Path>,
archive_path: impl AsRef<Path>,
folder: impl AsRef<Path>,
) -> Result<(), Error> {
let secret_key = pkgar_keys::get_skey(&secret_path.as_ref())?
.key()
.expect(&format!("{} was encrypted?", secret_path.as_ref().display()));
//TODO: move functions to library
......@@ -111,70 +93,81 @@ pub fn create(secret_path: &str, archive_path: &str, folder: &str) -> Result<(),
.write(true)
.create(true)
.truncate(true)
.open(archive_path)
.map_err(Error::Io)?;
.open(&archive_path)
.chain_err(|| archive_path.as_ref() )?;
// Create a list of entries
let mut entries = Vec::new();
folder_entries(folder, folder, &mut entries)
.map_err(Error::Io)?;
folder_entries(&folder, &folder, &mut entries)
.chain_err(|| folder.as_ref() )
.chain_err(|| "Recursing buildroot" )?;
// Create initial header
let mut header = Header {
signature: [0; 64],
public_key: secret_key.public_key().into_data(),
public_key: [0; 32],
blake3: [0; 32],
count: entries.len() as u64
};
header.public_key.copy_from_slice(secret_key.public_key().as_ref());
// Assign offsets to each entry
let mut data_size: u64 = 0;
for entry in &mut entries {
entry.offset = data_size;
data_size = data_size.checked_add(entry.size)
.ok_or(Error::Overflow)?;
.ok_or(pkgar_core::Error::Overflow)
.map_err(Error::from)
.chain_err(|| ErrorKind::Entry(*entry) )?;
}
// Seek to data offset
let data_offset = header.total_size()?;
archive_file.seek(SeekFrom::Start(data_offset as u64))
.map_err(Error::Io)?;
.chain_err(|| archive_path.as_ref() )
.chain_err(|| format!("Seek to {} (data offset)", data_offset) )?;
//TODO: fallocate data_offset + data_size
// Stream each file, writing data and calculating b3sums
let mut header_hasher = blake3::Hasher::new();
let mut buf = vec![0; 4 * 1024 * 1024];
for entry in &mut entries {
let relative = Path::new(OsStr::from_bytes(entry.path()));
let path = Path::new(folder).join(relative);
let relative = entry.check_path()?;
let path = folder.as_ref().join(relative);
let mode_kind = entry.mode & MODE_KIND;
let (total, hash) = match mode_kind {
MODE_FILE => {
let mode = entry.mode()
.map_err(Error::from)
.chain_err(|| ErrorKind::Entry(*entry) )?;
let (total, hash) = match mode.kind() {
Mode::FILE => {
let mut entry_file = fs::OpenOptions::new()
.read(true)
.open(path)
.map_err(Error::Io)?;
copy_hash(&mut entry_file, &mut archive_file, &mut buf)?
.open(&path)
.chain_err(|| &path )?;
copy_and_hash(&mut entry_file, &mut archive_file, &mut buf)
.chain_err(|| &path )
.chain_err(|| format!("Writing entry to archive: '{}'", relative.display()) )?
},
MODE_SYMLINK => {
let destination = fs::read_link(path)
.map_err(Error::Io)?;
Mode::SYMLINK => {
let destination = fs::read_link(&path)
.chain_err(|| &path )?;
let mut data = destination.as_os_str().as_bytes();
copy_hash(&mut data, &mut archive_file, &mut buf)?
copy_and_hash(&mut data, &mut archive_file, &mut buf)
.chain_err(|| &path )
.chain_err(|| format!("Writing entry to archive: '{}'", relative.display()) )?
},
_ => {
return Err(Error::Io(io::Error::new(
io::ErrorKind::InvalidInput,
format!("Unsupported mode {:#o}", { entry.mode })
)));
}
_ => return Err(Error::from(
pkgar_core::Error::InvalidMode(mode.bits())
))
.chain_err(|| ErrorKind::Entry(*entry) ),
};
if total != { entry.size } {
return Err(Error::Io(io::Error::new(
io::ErrorKind::InvalidInput,
format!("Copied {} instead of {}", total, { entry.size })
)));
if total != entry.size() {
return Err(Error::from_kind(ErrorKind::LengthMismatch(total, entry.size())))
.chain_err(|| ErrorKind::Entry(*entry) );
}
entry.blake3.copy_from_slice(hash.as_bytes());
......@@ -186,208 +179,140 @@ pub fn create(secret_path: &str, archive_path: &str, folder: &str) -> Result<(),
//TODO: ensure file size matches
// Calculate signature
let unsigned = header.clone();
sodalite::sign_attached(
unsafe { plain::as_mut_bytes(&mut header) },
unsafe { &plain::as_bytes(&unsigned)[64..] },
secret_key.as_data()
);
header.signature = sign::sign_detached(unsafe { &plain::as_bytes(&header)[64..] }, &secret_key).0;
// Write archive header
archive_file.seek(SeekFrom::Start(0))
.map_err(Error::Io)?;
.chain_err(|| archive_path.as_ref() )?;
archive_file.write_all(unsafe {
plain::as_bytes(&header)
}).map_err(Error::Io)?;
})
.chain_err(|| archive_path.as_ref() )?;
// Write each entry header
for entry in &entries {
let checked_path = entry.check_path()?;
archive_file.write_all(unsafe {
plain::as_bytes(entry)
}).map_err(Error::Io)?;
})
.chain_err(|| archive_path.as_ref() )
.chain_err(|| format!("Write entry {}", checked_path.display()) )?;
}
Ok(())
}
pub fn extract(public_path: &str, archive_path: &str, folder: &str) -> Result<(), Error> {
let public_key = {
let mut data = [0; 32];
fs::OpenOptions::new()
.read(true)
.open(public_path)
.map_err(Error::Io)?
.read_exact(&mut data)
.map_err(Error::Io)?;
PublicKey::from_data(data)
};
pub fn extract(
pkey_path: impl AsRef<Path>,
archive_path: impl AsRef<Path>,
base_dir: impl AsRef<Path>,
) -> Result<(), Error> {
let pkey = PublicKeyFile::open(&pkey_path.as_ref())?.pkey;
let mut archive_file = fs::OpenOptions::new()
.read(true)
.open(archive_path)
.map_err(Error::Io)?;
let mut package = PackageFile::new(archive_path, &pkey)?;
let mut package = Package::new(
PackageSrc::File(&mut archive_file),
&public_key
)?;
let entries = package.entries()?;
Transaction::install(&mut package, base_dir)?
.commit()?;
// TODO: Validate that all entries can be installed, before installing
Ok(())
}
let folder_path = Path::new(folder);
let mut buf = vec![0; 4 * 1024 * 1024];
let mut renames = Vec::new();
for entry in entries {
let relative = Path::new(OsStr::from_bytes(entry.path()));
for component in relative.components() {
match component {
Component::Normal(_) => (),
invalid => {
return Err(Error::Io(io::Error::new(
io::ErrorKind::InvalidData,
format!("entry path contains invalid component: {:?}", invalid)
)));
}
}
}
pub fn remove(
pkey_path: impl AsRef<Path>,
archive_path: impl AsRef<Path>,
base_dir: impl AsRef<Path>,
) -> Result<(), Error> {
let pkey = PublicKeyFile::open(&pkey_path.as_ref())?.pkey;
let entry_path = folder_path.join(relative);
if ! entry_path.starts_with(&folder_path) {
return Err(Error::Io(io::Error::new(
io::ErrorKind::InvalidData,
format!("entry path escapes from folder: {:?}", relative)
)));
}
let mut package = PackageFile::new(archive_path, &pkey)?;
let entry_hash = Hash::from(entry.hash());
let temp_name = if let Some(file_name) = entry_path.file_name().and_then(|x| x.to_str())
{
format!(".pkgar.{}", file_name)
} else {
format!(".pkgar.{}", entry_hash.to_hex())
};
let temp_path = if let Some(parent) = entry_path.parent() {
fs::create_dir_all(parent)
.map_err(Error::Io)?;
parent.join(temp_name)
} else {
return Err(Error::Io(io::Error::new(
io::ErrorKind::InvalidData,
format!("entry path has no parent: {:?}", entry_path)
)));
};
Transaction::remove(&mut package, base_dir)?
.commit()?;
let mode = entry.mode();
let mode_kind = mode & MODE_KIND;
let mode_perm = mode & MODE_PERM;
let (total, hash) = match mode_kind {
MODE_FILE => {
//TODO: decide what to do when temp files are left over
let mut temp_file = fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(mode_perm)
.open(&temp_path)
.map_err(Error::Io)?;
entry.copy_hash(&mut package, &mut temp_file, &mut buf)?
},
MODE_SYMLINK => {
let mut data = Vec::new();
let (total, hash) = entry.copy_hash(&mut package, &mut data, &mut buf)?;
let os_str: &OsStr = OsStrExt::from_bytes(data.as_slice());
symlink(os_str, &temp_path)
.map_err(Error::Io)?;
(total, hash)
},
_ => {
return Err(Error::Io(io::Error::new(
io::ErrorKind::InvalidData,
format!("Unsupported mode {:#o}", mode)
)));
}
};
if total != entry.size() {
return Err(Error::Io(io::Error::new(
io::ErrorKind::InvalidInput,
format!("Copied {} instead of {}", total, entry.size())
)));
}
if entry_hash != hash {
let _ = fs::remove_file(temp_path);
return Err(Error::InvalidBlake3);
}
Ok(())
}
renames.push((temp_path, entry_path));
}
pub fn list(
pkey_path: impl AsRef<Path>,
archive_path: impl AsRef<Path>,
) -> Result<(), Error> {
let pkey = PublicKeyFile::open(&pkey_path.as_ref())?.pkey;
for (temp_path, entry_path) in renames {
fs::rename(&temp_path, &entry_path)
.map_err(Error::Io)?;
let mut package = PackageFile::new(archive_path, &pkey)?;
for entry in package.read_entries()? {
let relative = entry.check_path()?;
println!("{}", relative.display());
}
Ok(())
}
#[cfg(feature = "rand")]
pub fn keygen(secret_path: &str, public_path: &str) -> Result<(), Error> {
use rand::rngs::OsRng;
pub fn split(
pkey_path: impl AsRef<Path>,
archive_path: impl AsRef<Path>,
) -> Result<(), Error> {
let pkey = PublicKeyFile::open(&pkey_path.as_ref())?.pkey;
let package = PackageFile::new(&archive_path, &pkey)?;
let data_offset = package.header().total_size()?;
let mut src = package.src.into_inner();
{
let data_path = archive_path.as_ref().with_extension("pkgar_data");
let mut data_file = fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&data_path)
.chain_err(|| &data_path )?;
src.seek(SeekFrom::Start(data_offset))
.chain_err(|| archive_path.as_ref())?;
io::copy(&mut src, &mut data_file)
.chain_err(|| archive_path.as_ref())
.chain_err(|| &data_path)?;
}
let secret_key = SecretKey::new(&mut OsRng)
.map_err(Error::Rand)?;
fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(0o400)
.open(secret_path)
.map_err(Error::Io)?
.write_all(secret_key.as_data())
.map_err(Error::Io)?;
let public_key = secret_key.public_key();
fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(0o400)
.open(public_path)
.map_err(Error::Io)?
.write_all(public_key.as_data())
.map_err(Error::Io)?;
{
let head_path = archive_path.as_ref().with_extension("pkgar_head");
let mut head_file = fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&head_path)
.chain_err(|| &head_path )?;
src.seek(SeekFrom::Start(0))
.chain_err(|| archive_path.as_ref())?;
io::copy(&mut src.take(data_offset), &mut head_file)
.chain_err(|| archive_path.as_ref())
.chain_err(|| &head_path)?;
}
Ok(())
}
pub fn list(public_path: &str, archive_path: &str) -> Result<(), Error> {
let public_key = {
let mut data = [0; 32];
fs::OpenOptions::new()
.read(true)
.open(public_path)
.map_err(Error::Io)?
.read_exact(&mut data)
.map_err(Error::Io)?;
PublicKey::from_data(data)
};
pub fn verify(
pkey_path: impl AsRef<Path>,
archive_path: impl AsRef<Path>,
base_dir: impl AsRef<Path>,
) -> Result<(), Error> {
let pkey = PublicKeyFile::open(pkey_path)?.pkey;
let mut archive_file = fs::OpenOptions::new()
.read(true)
.open(archive_path)
.map_err(Error::Io)?;
// Read header first
let mut package = Package::new(
PackageSrc::File(&mut archive_file),
&public_key
)?;
let entries = package.entries()?;
for entry in entries {
let relative = Path::new(OsStr::from_bytes(entry.path()));
println!("{}", relative.display());
}
let mut package = PackageFile::new(archive_path, &pkey)?;
let mut buf = vec![0; READ_WRITE_HASH_BUF_SIZE];
for entry in package.read_entries()? {
let expected_path = base_dir.as_ref()
.join(entry.check_path()?);
let expected = File::open(&expected_path)
.chain_err(|| &expected_path )?;
let (count, hash) = copy_and_hash(expected, io::sink(), &mut buf)?;
entry.verify(hash, count)?;
}
Ok(())
}
//! Extention traits for base types defined in `pkgar-core`.
use std::io::{self, Read, Write};
use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt;
use std::path::{Component, Path};
use blake3::{Hash, Hasher};
use pkgar_core::{Entry, PackageSrc};
use crate::{Error, ErrorKind, ResultExt};
/// Handy associated functions for `pkgar_core::Entry` that depend on std
pub trait EntryExt {
fn check_path(&self) -> Result<&Path, Error>;
fn verify(&self, blake3: Hash, size: u64) -> Result<(), Error>;
}
impl EntryExt for Entry {
/// Iterate the components of the path and ensure that there are no
/// non-normal components.
fn check_path(&self) -> Result<&Path, Error> {
let path = Path::new(OsStr::from_bytes(self.path_bytes()));
for component in path.components() {
match component {
Component::Normal(_) => {},
invalid => {
let bad_component: &Path = invalid.as_ref();
return Err(Error::from_kind(
ErrorKind::InvalidPathComponent(bad_component.to_path_buf())
))
.chain_err(|| ErrorKind::Entry(*self) );
},
}
}
Ok(&path)
}
fn verify(&self, blake3: Hash, size: u64) -> Result<(), Error> {
if size != self.size() {
Err(Error::from_kind(ErrorKind::LengthMismatch(size, self.size())))
.chain_err(|| ErrorKind::Entry(*self) )
} else if blake3 != self.blake3() {
Err(pkgar_core::Error::InvalidBlake3.into())
} else {
Ok(())
}
}
}
pub trait PackageSrcExt
where Self: PackageSrc + Sized,
{
/// Get the path corresponding to this `PackageSrc`. This will likely be
/// refactored to use something more generic than `Path` in future.
fn path(&self) -> &Path;
/// Build a reader for a given entry on this source.
fn entry_reader(&mut self, entry: Entry) -> EntryReader<'_, Self> {
EntryReader {
src: self,
entry,
pos: 0,
}
}
}
/// A reader that provides acess to one entry's data within a `PackageSrc`.
/// Use `PackageSrcExt::entry_reader` for construction
pub struct EntryReader<'a, Src>
where Src: PackageSrc
{
src: &'a mut Src,
entry: Entry,
pos: usize,
}
impl<Src, E> Read for EntryReader<'_, Src>
where
Src: PackageSrc<Err = E>,
E: From<pkgar_core::Error> + std::error::Error,
{
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let count = self.src.read_entry(self.entry, self.pos, buf)
// This is a little painful, since e is pkgar::Error...
// However, this is likely to be a very rarely triggered error
// condition.
.map_err(|err|
io::Error::new(io::ErrorKind::Other, err.to_string())
)?;
self.pos += count;
Ok(count)
}
}
/// Copy the contents of `read` into `write` by streaming through buf.
/// The basic function of this function is analogous to io::copy, except it
/// outputs the blake3 hash of the data streamed, and also does not allocate.
pub(crate) fn copy_and_hash<R: Read, W: Write>(
mut read: R,
mut write: W,
buf: &mut [u8]
) -> Result<(u64, Hash), io::Error> {
let mut hasher = Hasher::new();
let mut written = 0;
loop {
let count = read.read(buf)?;
if count == 0 {
break;
}
written += count as u64;
hasher.update_with_join::<blake3::join::RayonJoin>(&buf[..count]);
write.write_all(&buf[..count])?;
}
Ok((written, hasher.finalize()))
}