Build secure Rust watchOS TOTP core (#56)

This commit is contained in:
2026-08-16 14:27:21 +02:00
parent a055c93b86
commit df1d49339c
20 changed files with 2672 additions and 137 deletions

8
Cargo.lock generated
View File

@@ -5135,6 +5135,14 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "ironstorage-watch-apple"
version = "0.1.0"
dependencies = [
"ironstorage",
"uniffi",
]
[[package]] [[package]]
name = "is_executable" name = "is_executable"
version = "1.0.6" version = "1.0.6"

View File

@@ -5,6 +5,7 @@ members = [
"apps/tui", "apps/tui",
"crates/apple", "crates/apple",
"crates/storage", "crates/storage",
"crates/watch-apple",
"tools/macos-packager", "tools/macos-packager",
] ]
resolver = "3" resolver = "3"

View File

@@ -0,0 +1,7 @@
module ironstorage_watch {
header "ironstorage_watchFFI.h"
export *
use "Darwin"
use "_Builtin_stdbool"
use "_Builtin_stdint"
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,578 @@
// This file was autogenerated by some hot garbage in the `uniffi` crate.
// Trust me, you don't want to mess with it!
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
// The following structs are used to implement the lowest level
// of the FFI, and thus useful to multiple uniffied crates.
// We ensure they are declared exactly once, with a header guard, UNIFFI_SHARED_H.
#ifdef UNIFFI_SHARED_H
// We also try to prevent mixing versions of shared uniffi header structs.
// If you add anything to the #else block, you must increment the version suffix in UNIFFI_SHARED_HEADER_V4
#ifndef UNIFFI_SHARED_HEADER_V4
#error Combining helper code from multiple versions of uniffi is not supported
#endif // ndef UNIFFI_SHARED_HEADER_V4
#else
#define UNIFFI_SHARED_H
#define UNIFFI_SHARED_HEADER_V4
// ⚠️ Attention: If you change this #else block (ending in `#endif // def UNIFFI_SHARED_H`) you *must* ⚠️
// ⚠️ increment the version suffix in all instances of UNIFFI_SHARED_HEADER_V4 in this file. ⚠️
typedef struct RustBuffer
{
uint64_t capacity;
uint64_t len;
uint8_t *_Nullable data;
} RustBuffer;
typedef struct ForeignBytes
{
int32_t len;
const uint8_t *_Nullable data;
} ForeignBytes;
// Error definitions
typedef struct RustCallStatus {
int8_t code;
RustBuffer errorBuf;
} RustCallStatus;
// ⚠️ Attention: If you change this #else block (ending in `#endif // def UNIFFI_SHARED_H`) you *must* ⚠️
// ⚠️ increment the version suffix in all instances of UNIFFI_SHARED_HEADER_V4 in this file. ⚠️
#endif // def UNIFFI_SHARED_H
#ifndef UNIFFI_FFIDEF_RUST_FUTURE_CONTINUATION_CALLBACK
#define UNIFFI_FFIDEF_RUST_FUTURE_CONTINUATION_CALLBACK
typedef void (*UniffiRustFutureContinuationCallback)(uint64_t, int8_t
);
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_DROPPED_CALLBACK
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_DROPPED_CALLBACK
typedef void (*UniffiForeignFutureDroppedCallback)(uint64_t
);
#endif
#ifndef UNIFFI_FFIDEF_CALLBACK_INTERFACE_FREE
#define UNIFFI_FFIDEF_CALLBACK_INTERFACE_FREE
typedef void (*UniffiCallbackInterfaceFree)(uint64_t
);
#endif
#ifndef UNIFFI_FFIDEF_CALLBACK_INTERFACE_CLONE
#define UNIFFI_FFIDEF_CALLBACK_INTERFACE_CLONE
typedef uint64_t (*UniffiCallbackInterfaceClone)(uint64_t
);
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_DROPPED_CALLBACK_STRUCT
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_DROPPED_CALLBACK_STRUCT
typedef struct UniffiForeignFutureDroppedCallbackStruct {
uint64_t handle;
UniffiForeignFutureDroppedCallback _Nonnull free;
} UniffiForeignFutureDroppedCallbackStruct;
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U8
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U8
typedef struct UniffiForeignFutureResultU8 {
uint8_t returnValue;
RustCallStatus callStatus;
} UniffiForeignFutureResultU8;
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U8
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U8
typedef void (*UniffiForeignFutureCompleteU8)(uint64_t, UniffiForeignFutureResultU8
);
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I8
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I8
typedef struct UniffiForeignFutureResultI8 {
int8_t returnValue;
RustCallStatus callStatus;
} UniffiForeignFutureResultI8;
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I8
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I8
typedef void (*UniffiForeignFutureCompleteI8)(uint64_t, UniffiForeignFutureResultI8
);
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U16
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U16
typedef struct UniffiForeignFutureResultU16 {
uint16_t returnValue;
RustCallStatus callStatus;
} UniffiForeignFutureResultU16;
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U16
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U16
typedef void (*UniffiForeignFutureCompleteU16)(uint64_t, UniffiForeignFutureResultU16
);
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I16
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I16
typedef struct UniffiForeignFutureResultI16 {
int16_t returnValue;
RustCallStatus callStatus;
} UniffiForeignFutureResultI16;
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I16
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I16
typedef void (*UniffiForeignFutureCompleteI16)(uint64_t, UniffiForeignFutureResultI16
);
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U32
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U32
typedef struct UniffiForeignFutureResultU32 {
uint32_t returnValue;
RustCallStatus callStatus;
} UniffiForeignFutureResultU32;
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U32
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U32
typedef void (*UniffiForeignFutureCompleteU32)(uint64_t, UniffiForeignFutureResultU32
);
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I32
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I32
typedef struct UniffiForeignFutureResultI32 {
int32_t returnValue;
RustCallStatus callStatus;
} UniffiForeignFutureResultI32;
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I32
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I32
typedef void (*UniffiForeignFutureCompleteI32)(uint64_t, UniffiForeignFutureResultI32
);
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U64
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U64
typedef struct UniffiForeignFutureResultU64 {
uint64_t returnValue;
RustCallStatus callStatus;
} UniffiForeignFutureResultU64;
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U64
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U64
typedef void (*UniffiForeignFutureCompleteU64)(uint64_t, UniffiForeignFutureResultU64
);
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I64
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I64
typedef struct UniffiForeignFutureResultI64 {
int64_t returnValue;
RustCallStatus callStatus;
} UniffiForeignFutureResultI64;
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I64
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I64
typedef void (*UniffiForeignFutureCompleteI64)(uint64_t, UniffiForeignFutureResultI64
);
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_F32
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_F32
typedef struct UniffiForeignFutureResultF32 {
float returnValue;
RustCallStatus callStatus;
} UniffiForeignFutureResultF32;
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_F32
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_F32
typedef void (*UniffiForeignFutureCompleteF32)(uint64_t, UniffiForeignFutureResultF32
);
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_F64
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_F64
typedef struct UniffiForeignFutureResultF64 {
double returnValue;
RustCallStatus callStatus;
} UniffiForeignFutureResultF64;
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_F64
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_F64
typedef void (*UniffiForeignFutureCompleteF64)(uint64_t, UniffiForeignFutureResultF64
);
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_RUST_BUFFER
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_RUST_BUFFER
typedef struct UniffiForeignFutureResultRustBuffer {
RustBuffer returnValue;
RustCallStatus callStatus;
} UniffiForeignFutureResultRustBuffer;
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_RUST_BUFFER
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_RUST_BUFFER
typedef void (*UniffiForeignFutureCompleteRustBuffer)(uint64_t, UniffiForeignFutureResultRustBuffer
);
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_VOID
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_VOID
typedef struct UniffiForeignFutureResultVoid {
RustCallStatus callStatus;
} UniffiForeignFutureResultVoid;
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_VOID
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_VOID
typedef void (*UniffiForeignFutureCompleteVoid)(uint64_t, UniffiForeignFutureResultVoid
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_CLONE_WATCHCORE
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_CLONE_WATCHCORE
uint64_t uniffi_ironstorage_watch_fn_clone_watchcore(uint64_t handle, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_FREE_WATCHCORE
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_FREE_WATCHCORE
void uniffi_ironstorage_watch_fn_free_watchcore(uint64_t handle, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_METHOD_WATCHCORE_APPLY_SNAPSHOT
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_METHOD_WATCHCORE_APPLY_SNAPSHOT
RustBuffer uniffi_ironstorage_watch_fn_method_watchcore_apply_snapshot(uint64_t ptr, RustBuffer snapshot, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_METHOD_WATCHCORE_NO_PERSISTED_SNAPSHOT
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_METHOD_WATCHCORE_NO_PERSISTED_SNAPSHOT
void uniffi_ironstorage_watch_fn_method_watchcore_no_persisted_snapshot(uint64_t ptr, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_METHOD_WATCHCORE_PROTECTED_DATA_UNAVAILABLE
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_METHOD_WATCHCORE_PROTECTED_DATA_UNAVAILABLE
void uniffi_ironstorage_watch_fn_method_watchcore_protected_data_unavailable(uint64_t ptr, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_METHOD_WATCHCORE_RECORDS_AT
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_METHOD_WATCHCORE_RECORDS_AT
RustBuffer uniffi_ironstorage_watch_fn_method_watchcore_records_at(uint64_t ptr, uint64_t unix_seconds, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_FUNC_WATCH_CORE
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_FUNC_WATCH_CORE
uint64_t uniffi_ironstorage_watch_fn_func_watch_core(RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUSTBUFFER_ALLOC
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUSTBUFFER_ALLOC
RustBuffer ffi_ironstorage_watch_rustbuffer_alloc(uint64_t size, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUSTBUFFER_FROM_BYTES
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUSTBUFFER_FROM_BYTES
RustBuffer ffi_ironstorage_watch_rustbuffer_from_bytes(ForeignBytes bytes, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUSTBUFFER_FREE
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUSTBUFFER_FREE
void ffi_ironstorage_watch_rustbuffer_free(RustBuffer buf, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUSTBUFFER_RESERVE
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUSTBUFFER_RESERVE
RustBuffer ffi_ironstorage_watch_rustbuffer_reserve(RustBuffer buf, uint64_t additional, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_POLL_U8
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_POLL_U8
void ffi_ironstorage_watch_rust_future_poll_u8(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_CANCEL_U8
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_CANCEL_U8
void ffi_ironstorage_watch_rust_future_cancel_u8(uint64_t handle
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_FREE_U8
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_FREE_U8
void ffi_ironstorage_watch_rust_future_free_u8(uint64_t handle
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_COMPLETE_U8
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_COMPLETE_U8
uint8_t ffi_ironstorage_watch_rust_future_complete_u8(uint64_t handle, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_POLL_I8
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_POLL_I8
void ffi_ironstorage_watch_rust_future_poll_i8(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_CANCEL_I8
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_CANCEL_I8
void ffi_ironstorage_watch_rust_future_cancel_i8(uint64_t handle
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_FREE_I8
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_FREE_I8
void ffi_ironstorage_watch_rust_future_free_i8(uint64_t handle
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_COMPLETE_I8
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_COMPLETE_I8
int8_t ffi_ironstorage_watch_rust_future_complete_i8(uint64_t handle, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_POLL_U16
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_POLL_U16
void ffi_ironstorage_watch_rust_future_poll_u16(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_CANCEL_U16
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_CANCEL_U16
void ffi_ironstorage_watch_rust_future_cancel_u16(uint64_t handle
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_FREE_U16
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_FREE_U16
void ffi_ironstorage_watch_rust_future_free_u16(uint64_t handle
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_COMPLETE_U16
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_COMPLETE_U16
uint16_t ffi_ironstorage_watch_rust_future_complete_u16(uint64_t handle, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_POLL_I16
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_POLL_I16
void ffi_ironstorage_watch_rust_future_poll_i16(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_CANCEL_I16
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_CANCEL_I16
void ffi_ironstorage_watch_rust_future_cancel_i16(uint64_t handle
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_FREE_I16
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_FREE_I16
void ffi_ironstorage_watch_rust_future_free_i16(uint64_t handle
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_COMPLETE_I16
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_COMPLETE_I16
int16_t ffi_ironstorage_watch_rust_future_complete_i16(uint64_t handle, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_POLL_U32
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_POLL_U32
void ffi_ironstorage_watch_rust_future_poll_u32(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_CANCEL_U32
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_CANCEL_U32
void ffi_ironstorage_watch_rust_future_cancel_u32(uint64_t handle
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_FREE_U32
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_FREE_U32
void ffi_ironstorage_watch_rust_future_free_u32(uint64_t handle
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_COMPLETE_U32
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_COMPLETE_U32
uint32_t ffi_ironstorage_watch_rust_future_complete_u32(uint64_t handle, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_POLL_I32
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_POLL_I32
void ffi_ironstorage_watch_rust_future_poll_i32(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_CANCEL_I32
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_CANCEL_I32
void ffi_ironstorage_watch_rust_future_cancel_i32(uint64_t handle
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_FREE_I32
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_FREE_I32
void ffi_ironstorage_watch_rust_future_free_i32(uint64_t handle
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_COMPLETE_I32
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_COMPLETE_I32
int32_t ffi_ironstorage_watch_rust_future_complete_i32(uint64_t handle, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_POLL_U64
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_POLL_U64
void ffi_ironstorage_watch_rust_future_poll_u64(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_CANCEL_U64
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_CANCEL_U64
void ffi_ironstorage_watch_rust_future_cancel_u64(uint64_t handle
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_FREE_U64
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_FREE_U64
void ffi_ironstorage_watch_rust_future_free_u64(uint64_t handle
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_COMPLETE_U64
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_COMPLETE_U64
uint64_t ffi_ironstorage_watch_rust_future_complete_u64(uint64_t handle, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_POLL_I64
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_POLL_I64
void ffi_ironstorage_watch_rust_future_poll_i64(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_CANCEL_I64
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_CANCEL_I64
void ffi_ironstorage_watch_rust_future_cancel_i64(uint64_t handle
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_FREE_I64
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_FREE_I64
void ffi_ironstorage_watch_rust_future_free_i64(uint64_t handle
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_COMPLETE_I64
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_COMPLETE_I64
int64_t ffi_ironstorage_watch_rust_future_complete_i64(uint64_t handle, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_POLL_F32
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_POLL_F32
void ffi_ironstorage_watch_rust_future_poll_f32(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_CANCEL_F32
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_CANCEL_F32
void ffi_ironstorage_watch_rust_future_cancel_f32(uint64_t handle
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_FREE_F32
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_FREE_F32
void ffi_ironstorage_watch_rust_future_free_f32(uint64_t handle
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_COMPLETE_F32
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_COMPLETE_F32
float ffi_ironstorage_watch_rust_future_complete_f32(uint64_t handle, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_POLL_F64
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_POLL_F64
void ffi_ironstorage_watch_rust_future_poll_f64(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_CANCEL_F64
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_CANCEL_F64
void ffi_ironstorage_watch_rust_future_cancel_f64(uint64_t handle
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_FREE_F64
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_FREE_F64
void ffi_ironstorage_watch_rust_future_free_f64(uint64_t handle
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_COMPLETE_F64
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_COMPLETE_F64
double ffi_ironstorage_watch_rust_future_complete_f64(uint64_t handle, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_POLL_RUST_BUFFER
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_POLL_RUST_BUFFER
void ffi_ironstorage_watch_rust_future_poll_rust_buffer(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_CANCEL_RUST_BUFFER
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_CANCEL_RUST_BUFFER
void ffi_ironstorage_watch_rust_future_cancel_rust_buffer(uint64_t handle
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_FREE_RUST_BUFFER
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_FREE_RUST_BUFFER
void ffi_ironstorage_watch_rust_future_free_rust_buffer(uint64_t handle
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_COMPLETE_RUST_BUFFER
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_COMPLETE_RUST_BUFFER
RustBuffer ffi_ironstorage_watch_rust_future_complete_rust_buffer(uint64_t handle, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_POLL_VOID
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_POLL_VOID
void ffi_ironstorage_watch_rust_future_poll_void(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_CANCEL_VOID
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_CANCEL_VOID
void ffi_ironstorage_watch_rust_future_cancel_void(uint64_t handle
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_FREE_VOID
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_FREE_VOID
void ffi_ironstorage_watch_rust_future_free_void(uint64_t handle
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_COMPLETE_VOID
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_COMPLETE_VOID
void ffi_ironstorage_watch_rust_future_complete_void(uint64_t handle, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_CHECKSUM_FUNC_WATCH_CORE
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_CHECKSUM_FUNC_WATCH_CORE
uint16_t uniffi_ironstorage_watch_checksum_func_watch_core(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_CHECKSUM_METHOD_WATCHCORE_APPLY_SNAPSHOT
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_CHECKSUM_METHOD_WATCHCORE_APPLY_SNAPSHOT
uint16_t uniffi_ironstorage_watch_checksum_method_watchcore_apply_snapshot(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_CHECKSUM_METHOD_WATCHCORE_NO_PERSISTED_SNAPSHOT
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_CHECKSUM_METHOD_WATCHCORE_NO_PERSISTED_SNAPSHOT
uint16_t uniffi_ironstorage_watch_checksum_method_watchcore_no_persisted_snapshot(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_CHECKSUM_METHOD_WATCHCORE_PROTECTED_DATA_UNAVAILABLE
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_CHECKSUM_METHOD_WATCHCORE_PROTECTED_DATA_UNAVAILABLE
uint16_t uniffi_ironstorage_watch_checksum_method_watchcore_protected_data_unavailable(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_CHECKSUM_METHOD_WATCHCORE_RECORDS_AT
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_CHECKSUM_METHOD_WATCHCORE_RECORDS_AT
uint16_t uniffi_ironstorage_watch_checksum_method_watchcore_records_at(void
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_UNIFFI_CONTRACT_VERSION
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_UNIFFI_CONTRACT_VERSION
uint32_t ffi_ironstorage_watch_uniffi_contract_version(void
);
#endif

View File

@@ -0,0 +1 @@
#include "Generated/ironstorage_watchFFI.h"

View File

@@ -1,24 +1,122 @@
import Security
import SwiftUI import SwiftUI
import WatchConnectivity import WatchConnectivity
private enum SecureSnapshotStore {
private static let service = "de.rfc1437.ironstorage.watch.snapshot"
private static let account = "selected-totp"
static func load() throws -> Data? {
var result: CFTypeRef?
let status = SecItemCopyMatching([
kSecClass: kSecClassGenericPassword,
kSecAttrService: service,
kSecAttrAccount: account,
kSecReturnData: true,
kSecMatchLimit: kSecMatchLimitOne,
] as CFDictionary, &result)
if status == errSecItemNotFound { return nil }
guard status == errSecSuccess, let data = result as? Data else {
throw SnapshotStoreError(status: status)
}
return data
}
static func replace(_ snapshot: Data) throws {
let query = [
kSecClass: kSecClassGenericPassword,
kSecAttrService: service,
kSecAttrAccount: account,
] as CFDictionary
let status = SecItemUpdate(query, [kSecValueData: snapshot] as CFDictionary)
if status == errSecItemNotFound {
let added = SecItemAdd([
kSecClass: kSecClassGenericPassword,
kSecAttrService: service,
kSecAttrAccount: account,
kSecValueData: snapshot,
kSecAttrAccessible: kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
] as CFDictionary, nil)
guard added == errSecSuccess else { throw SnapshotStoreError(status: added) }
} else if status != errSecSuccess {
throw SnapshotStoreError(status: status)
}
}
static func delete() throws {
let status = SecItemDelete([
kSecClass: kSecClassGenericPassword,
kSecAttrService: service,
kSecAttrAccount: account,
] as CFDictionary)
guard status == errSecSuccess || status == errSecItemNotFound else {
throw SnapshotStoreError(status: status)
}
}
}
private struct SnapshotStoreError: Error {
let status: OSStatus
}
@MainActor
private final class WatchSnapshotTransport: NSObject, ObservableObject, WCSessionDelegate { private final class WatchSnapshotTransport: NSObject, ObservableObject, WCSessionDelegate {
private static let snapshotKey = "de.rfc1437.ironstorage.watch.snapshot" private static let snapshotKey = "de.rfc1437.ironstorage.watch.snapshot"
private static let deliveredReceiptKey = "de.rfc1437.ironstorage.watch.delivered" private static let receiptKey = "de.rfc1437.ironstorage.watch.delivered"
@Published private(set) var records: [WatchTotpRecord] = []
private let core = watchCore()
override init() { override init() {
super.init() super.init()
restoreSnapshot()
guard WCSession.isSupported() else { return } guard WCSession.isSupported() else { return }
let session = WCSession.default let session = WCSession.default
session.delegate = self session.delegate = self
session.activate() session.activate()
} }
func sceneBecameActive() {
restoreSnapshot()
}
func sceneBecameInactive() {
try? core.protectedDataUnavailable()
records.removeAll(keepingCapacity: false)
}
private func restoreSnapshot() {
do {
if var snapshot = try SecureSnapshotStore.load() {
defer { snapshot.resetBytes(in: 0..<snapshot.count) }
let update = try core.applySnapshot(snapshot: snapshot)
if update.persistence == .delete { try SecureSnapshotStore.delete() }
} else {
try core.noPersistedSnapshot()
}
refreshRecords()
} catch {
try? core.protectedDataUnavailable()
records.removeAll(keepingCapacity: false)
}
}
private func receive(_ applicationContext: [String: Any], session: WCSession) { private func receive(_ applicationContext: [String: Any], session: WCSession) {
guard guard var snapshot = applicationContext[Self.snapshotKey] as? Data else { return }
applicationContext[Self.snapshotKey] is Data, defer { snapshot.resetBytes(in: 0..<snapshot.count) }
let receipt = applicationContext[Self.deliveredReceiptKey] as? Data do {
else { return } let update = try core.applySnapshot(snapshot: snapshot)
let acknowledgement = [Self.deliveredReceiptKey: receipt] switch update.persistence {
case .keep:
break
case .replace:
try SecureSnapshotStore.replace(snapshot)
case .delete:
try SecureSnapshotStore.delete()
}
refreshRecords()
guard !update.receipt.isEmpty else { return }
let acknowledgement = [Self.receiptKey: update.receipt]
if session.isReachable { if session.isReachable {
session.sendMessage(acknowledgement, replyHandler: nil) { _ in session.sendMessage(acknowledgement, replyHandler: nil) { _ in
session.transferUserInfo(acknowledgement) session.transferUserInfo(acknowledgement)
@@ -26,32 +124,54 @@ private final class WatchSnapshotTransport: NSObject, ObservableObject, WCSessio
} else { } else {
session.transferUserInfo(acknowledgement) session.transferUserInfo(acknowledgement)
} }
} catch {
try? core.noPersistedSnapshot()
records.removeAll(keepingCapacity: false)
}
} }
func session( private func refreshRecords() {
records = (try? core.recordsAt(
unixSeconds: UInt64(Date().timeIntervalSince1970)
)) ?? []
}
nonisolated func session(
_ session: WCSession, _ session: WCSession,
activationDidCompleteWith activationState: WCSessionActivationState, activationDidCompleteWith activationState: WCSessionActivationState,
error: Error? error: Error?
) { ) {
guard activationState == .activated, error == nil else { return } guard activationState == .activated, error == nil else { return }
receive(session.receivedApplicationContext, session: session) Task { @MainActor [weak self] in
self?.receive(session.receivedApplicationContext, session: session)
}
} }
func session( nonisolated func session(
_ session: WCSession, _ session: WCSession,
didReceiveApplicationContext applicationContext: [String: Any] didReceiveApplicationContext applicationContext: [String: Any]
) { ) {
receive(applicationContext, session: session) Task { @MainActor [weak self] in
self?.receive(applicationContext, session: session)
}
} }
} }
@main @main
struct IronStorageWatchApp: App { struct IronStorageWatchApp: App {
@Environment(\.scenePhase) private var scenePhase
@StateObject private var transport = WatchSnapshotTransport() @StateObject private var transport = WatchSnapshotTransport()
var body: some Scene { var body: some Scene {
WindowGroup { WindowGroup {
ContentUnavailableView("No TOTP Codes", systemImage: "timer") ContentUnavailableView("No TOTP Codes", systemImage: "timer")
.onChange(of: scenePhase) { _, phase in
if phase == .active {
transport.sceneBecameActive()
} else {
transport.sceneBecameInactive()
}
}
} }
} }
} }

View File

@@ -11,28 +11,42 @@ else
profile=release profile=release
fi fi
if [[ "${PLATFORM_NAME:-}" == watch* ]]; then
package=ironstorage-watch-apple
library=ironstorage_watch
else
package=ironstorage-apple
library=ironstorage_apple
fi
libraries=() libraries=()
for arch in $ARCHS; do for arch in $ARCHS; do
if [[ "$arch" == "arm64" && "${LLVM_TARGET_TRIPLE_SUFFIX:-}" == "-simulator" ]]; then if [[ "$package" == "ironstorage-watch-apple" && "$arch" == "arm64" && "${LLVM_TARGET_TRIPLE_SUFFIX:-}" == "-simulator" ]]; then
target=aarch64-apple-watchos-sim
elif [[ "$package" == "ironstorage-watch-apple" && "$arch" == "arm64" ]]; then
target=aarch64-apple-watchos
elif [[ "$package" == "ironstorage-watch-apple" && "$arch" == "x86_64" ]]; then
target=x86_64-apple-watchos-sim
elif [[ "$arch" == "arm64" && "${LLVM_TARGET_TRIPLE_SUFFIX:-}" == "-simulator" ]]; then
target=aarch64-apple-ios-sim target=aarch64-apple-ios-sim
elif [[ "$arch" == "arm64" ]]; then elif [[ "$arch" == "arm64" ]]; then
target=aarch64-apple-ios target=aarch64-apple-ios
elif [[ "$arch" == "x86_64" ]]; then elif [[ "$arch" == "x86_64" ]]; then
target=x86_64-apple-ios target=x86_64-apple-ios
else else
echo "Unsupported iOS architecture: $arch" >&2 echo "Unsupported ${PLATFORM_NAME:-Apple} architecture: $arch" >&2
exit 1 exit 1
fi fi
if [[ "$profile" == "debug" ]]; then if [[ "$profile" == "debug" ]]; then
rustup run stable cargo build --locked \ rustup run stable cargo build --locked \
--target "$target" --package ironstorage-apple --lib --target "$target" --package "$package" --lib
else else
rustup run stable cargo build --locked --release \ rustup run stable cargo build --locked --release \
--target "$target" --package ironstorage-apple --lib --target "$target" --package "$package" --lib
fi fi
libraries+=("$CARGO_TARGET_DIR/$target/$profile/libironstorage_apple.a") libraries+=("$CARGO_TARGET_DIR/$target/$profile/lib$library.a")
done done
mkdir -p "$DERIVED_FILE_DIR/rust" mkdir -p "$DERIVED_FILE_DIR/rust"
lipo -create -output "$DERIVED_FILE_DIR/rust/libironstorage_apple.a" "${libraries[@]}" lipo -create -output "$DERIVED_FILE_DIR/rust/lib$library.a" "${libraries[@]}"

View File

@@ -77,6 +77,10 @@ targets:
PRODUCT_BUNDLE_IDENTIFIER: de.rfc1437.ironstorage.watch PRODUCT_BUNDLE_IDENTIFIER: de.rfc1437.ironstorage.watch
PRODUCT_NAME: IronStorage Watch PRODUCT_NAME: IronStorage Watch
SKIP_INSTALL: YES SKIP_INSTALL: YES
"EXCLUDED_ARCHS[sdk=watchsimulator*]": x86_64
SWIFT_OBJC_BRIDGING_HEADER: IronStorage-Watch-Bridging-Header.h
LIBRARY_SEARCH_PATHS: "$(inherited) $(DERIVED_FILE_DIR)/rust"
OTHER_LDFLAGS: "$(inherited) -lironstorage_watch"
info: info:
path: Watch-Info.plist path: Watch-Info.plist
properties: properties:
@@ -86,3 +90,11 @@ targets:
sources: sources:
- Assets.xcassets - Assets.xcassets
- Sources/Watch - Sources/Watch
- Generated/ironstorage_watch.swift
preBuildScripts:
- name: Build Rust Watch core
basedOnDependencyAnalysis: false
script: |
./build_rust_core.bash
outputFiles:
- $(DERIVED_FILE_DIR)/rust/libironstorage_watch.a

View File

@@ -6,47 +6,79 @@ edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
publish = false publish = false
[features]
default = ["full"]
full = [
"dep:apple-native-keyring-store",
"dep:arboard",
"dep:cap-std",
"dep:cap-tempfile",
"dep:clap",
"dep:clap_complete",
"dep:flate2",
"dep:gix",
"dep:gix-config",
"dep:image",
"dep:keepass",
"dep:keyring-core",
"dep:pgp",
"dep:qrcode",
"dep:rand",
"dep:regex",
"dep:reqwest",
"dep:rqrr",
"dep:secret-service",
"dep:security-framework",
"dep:serde",
"dep:shlex",
"dep:toml",
"dep:url",
"dep:windows-native-keyring-store",
"dep:zbus-secret-service-keyring-store",
]
watch = []
[dependencies] [dependencies]
cap-std.workspace = true cap-std = { workspace = true, optional = true }
cap-tempfile.workspace = true cap-tempfile = { workspace = true, optional = true }
clap.workspace = true clap = { workspace = true, optional = true }
clap_complete.workspace = true clap_complete = { workspace = true, optional = true }
data-encoding.workspace = true data-encoding.workspace = true
flate2.workspace = true flate2 = { workspace = true, optional = true }
gix.workspace = true gix = { workspace = true, optional = true }
gix-config.workspace = true gix-config = { workspace = true, optional = true }
hmac.workspace = true hmac.workspace = true
image.workspace = true image = { workspace = true, optional = true }
keyring-core.workspace = true keyring-core = { workspace = true, optional = true }
keepass.workspace = true keepass = { workspace = true, optional = true }
pgp.workspace = true pgp = { workspace = true, optional = true }
qrcode.workspace = true qrcode = { workspace = true, optional = true }
rand.workspace = true rand = { workspace = true, optional = true }
regex.workspace = true regex = { workspace = true, optional = true }
reqwest.workspace = true reqwest = { workspace = true, optional = true }
rqrr.workspace = true rqrr = { workspace = true, optional = true }
serde.workspace = true serde = { workspace = true, optional = true }
sha1.workspace = true sha1.workspace = true
sha2.workspace = true sha2.workspace = true
shlex.workspace = true shlex = { workspace = true, optional = true }
toml.workspace = true toml = { workspace = true, optional = true }
url.workspace = true url = { workspace = true, optional = true }
zeroize.workspace = true zeroize.workspace = true
[target.'cfg(any(target_os = "ios", target_os = "macos"))'.dependencies] [target.'cfg(any(target_os = "ios", target_os = "macos"))'.dependencies]
apple-native-keyring-store.workspace = true apple-native-keyring-store = { workspace = true, optional = true }
security-framework.workspace = true security-framework = { workspace = true, optional = true }
[target.'cfg(target_os = "windows")'.dependencies] [target.'cfg(target_os = "windows")'.dependencies]
windows-native-keyring-store.workspace = true windows-native-keyring-store = { workspace = true, optional = true }
[target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]
arboard.workspace = true arboard = { workspace = true, optional = true }
secret-service.workspace = true secret-service = { workspace = true, optional = true }
zbus-secret-service-keyring-store.workspace = true zbus-secret-service-keyring-store = { workspace = true, optional = true }
[target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies] [target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies]
arboard.workspace = true arboard = { workspace = true, optional = true }
[dev-dependencies] [dev-dependencies]
hex = "0.4" hex = "0.4"

View File

@@ -5,32 +5,60 @@
//! //!
//! This crate is the sole owner of stored and derived password-store objects. //! This crate is the sole owner of stored and derived password-store objects.
#[cfg(feature = "full")]
pub mod authentication; pub mod authentication;
#[cfg(feature = "full")]
pub mod command; pub mod command;
#[cfg(feature = "full")]
pub mod config; pub mod config;
#[cfg(feature = "full")]
pub mod crypto; pub mod crypto;
#[cfg(feature = "full")]
pub mod desktop; pub mod desktop;
#[cfg(feature = "full")]
pub mod document; pub mod document;
#[cfg(feature = "full")]
pub mod generate; pub mod generate;
#[cfg(feature = "full")]
pub mod git; pub mod git;
#[cfg(feature = "full")]
pub mod kdbx; pub mod kdbx;
#[cfg(feature = "full")]
pub mod mobile; pub mod mobile;
#[cfg(feature = "full")]
pub mod mobile_authentication; pub mod mobile_authentication;
#[cfg(feature = "full")]
pub mod mobile_entry; pub mod mobile_entry;
#[cfg(feature = "full")]
pub mod mobile_home; pub mod mobile_home;
#[cfg(feature = "full")]
pub mod mobile_key_transfer; pub mod mobile_key_transfer;
#[cfg(feature = "full")]
pub mod mobile_mutation; pub mod mobile_mutation;
#[cfg(feature = "full")]
pub mod mobile_onboarding; pub mod mobile_onboarding;
#[cfg(feature = "full")]
pub mod mobile_passwords; pub mod mobile_passwords;
#[cfg(feature = "full")]
pub mod mobile_totp; pub mod mobile_totp;
pub mod mobile_watch; pub mod mobile_watch;
#[cfg(feature = "full")]
pub mod mutation; pub mod mutation;
#[cfg(feature = "full")]
pub mod otp; pub mod otp;
mod otp_core;
#[cfg(feature = "full")]
pub mod presentation; pub mod presentation;
#[cfg(feature = "full")]
pub mod read; pub mod read;
#[cfg(feature = "full")]
pub mod recipient; pub mod recipient;
#[cfg(feature = "full")]
pub mod repository; pub mod repository;
mod secret;
#[cfg(feature = "full")]
pub mod secret_store; pub mod secret_store;
#[cfg(feature = "full")]
pub mod write; pub mod write;
/// Product name shared by the presentation adapters. /// Product name shared by the presentation adapters.

View File

@@ -521,7 +521,7 @@ impl<'a> MobileTotpService<'a> {
let period = uri.period().ok_or(OtpError::NotTotp)?; let period = uri.period().ok_or(OtpError::NotTotp)?;
retained.insert(path.clone()); retained.insert(path.clone());
entries.push(WatchSnapshotEntry::new( entries.push(WatchSnapshotEntry::new(
path.clone(), path.to_string(),
uri.issuer().map(str::to_owned), uri.issuer().map(str::to_owned),
uri.account().to_owned(), uri.account().to_owned(),
uri.algorithm(), uri.algorithm(),

View File

@@ -1,26 +1,30 @@
//! Versioned, replacement-only Apple Watch TOTP snapshots and sender state. //! Versioned Apple Watch TOTP snapshots and the minimal offline Watch runtime.
use std::{ use std::{error::Error, fmt, path::Path};
error::Error,
fmt, fs,
io::Write as _,
path::{Path, PathBuf},
};
#[cfg(feature = "full")]
use cap_std::{ambient_authority, fs::Dir}; use cap_std::{ambient_authority, fs::Dir};
#[cfg(feature = "full")]
use cap_tempfile::TempFile; use cap_tempfile::TempFile;
use data_encoding::{HEXLOWER, HEXLOWER_PERMISSIVE}; use data_encoding::HEXLOWER;
#[cfg(feature = "full")]
use data_encoding::HEXLOWER_PERMISSIVE;
#[cfg(feature = "full")]
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sha2::{Digest as _, Sha256}; use sha2::{Digest as _, Sha256};
#[cfg(feature = "full")]
use std::{fs, io::Write as _, path::PathBuf};
use crate::{ use crate::{
otp::OtpAlgorithm, otp_core::{self, OtpAlgorithm},
repository::{EntryPath, SecretBytes}, secret::SecretBytes,
}; };
const SNAPSHOT_MAGIC: &[u8; 4] = b"ISWS"; const SNAPSHOT_MAGIC: &[u8; 4] = b"ISWS";
const RECEIPT_MAGIC: &[u8; 4] = b"ISWR"; const RECEIPT_MAGIC: &[u8; 4] = b"ISWR";
const SNAPSHOT_VERSION: u16 = 1; const SNAPSHOT_VERSION: u16 = 1;
#[cfg(feature = "full")]
const JOURNAL_VERSION: u32 = 1; const JOURNAL_VERSION: u32 = 1;
const MAX_SNAPSHOT_BYTES: usize = 256 * 1024; const MAX_SNAPSHOT_BYTES: usize = 256 * 1024;
const MAX_ENTRIES: usize = 256; const MAX_ENTRIES: usize = 256;
@@ -57,7 +61,7 @@ impl MobileWatchSnapshotStatus {
} }
pub struct WatchSnapshotEntry { pub struct WatchSnapshotEntry {
path: EntryPath, path: String,
issuer: Option<String>, issuer: Option<String>,
account: String, account: String,
algorithm: OtpAlgorithm, algorithm: OtpAlgorithm,
@@ -68,7 +72,7 @@ pub struct WatchSnapshotEntry {
impl WatchSnapshotEntry { impl WatchSnapshotEntry {
pub fn new( pub fn new(
path: EntryPath, path: String,
issuer: Option<String>, issuer: Option<String>,
account: String, account: String,
algorithm: OtpAlgorithm, algorithm: OtpAlgorithm,
@@ -87,7 +91,7 @@ impl WatchSnapshotEntry {
} }
} }
pub fn path(&self) -> &EntryPath { pub fn path(&self) -> &str {
&self.path &self.path
} }
pub fn issuer(&self) -> Option<&str> { pub fn issuer(&self) -> Option<&str> {
@@ -156,6 +160,7 @@ impl fmt::Debug for WatchSnapshot {
} }
} }
#[cfg(feature = "full")]
pub struct WatchSnapshotTransfer { pub struct WatchSnapshotTransfer {
revision: u64, revision: u64,
selected_entries: u32, selected_entries: u32,
@@ -163,6 +168,7 @@ pub struct WatchSnapshotTransfer {
delivered_receipt: Vec<u8>, delivered_receipt: Vec<u8>,
} }
#[cfg(feature = "full")]
impl WatchSnapshotTransfer { impl WatchSnapshotTransfer {
pub fn revision(&self) -> u64 { pub fn revision(&self) -> u64 {
self.revision self.revision
@@ -178,6 +184,7 @@ impl WatchSnapshotTransfer {
} }
} }
#[cfg(feature = "full")]
impl fmt::Debug for WatchSnapshotTransfer { impl fmt::Debug for WatchSnapshotTransfer {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter formatter
@@ -273,14 +280,184 @@ impl WatchSnapshotReceiver {
self.current = None; self.current = None;
self.accepted = None; self.accepted = None;
} }
pub fn clear_secrets(&mut self) {
self.current = None;
}
} }
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum WatchPersistenceAction {
Keep,
Replace,
Delete,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WatchSnapshotUpdate {
apply: WatchSnapshotApply,
persistence: WatchPersistenceAction,
revision: Option<u64>,
selected_entries: u32,
receipt: Vec<u8>,
}
impl WatchSnapshotUpdate {
pub fn apply(&self) -> WatchSnapshotApply {
self.apply
}
pub fn persistence(&self) -> WatchPersistenceAction {
self.persistence
}
pub fn revision(&self) -> Option<u64> {
self.revision
}
pub fn selected_entries(&self) -> u32 {
self.selected_entries
}
pub fn receipt(&self) -> &[u8] {
&self.receipt
}
}
pub struct WatchTotpRecord {
path: String,
issuer: Option<String>,
account: String,
code: SecretBytes,
period: u64,
valid_until: u64,
}
impl WatchTotpRecord {
pub fn path(&self) -> &str {
&self.path
}
pub fn issuer(&self) -> Option<&str> {
self.issuer.as_deref()
}
pub fn account(&self) -> &str {
&self.account
}
pub fn code(&self) -> &SecretBytes {
&self.code
}
pub fn period(&self) -> u64 {
self.period
}
pub fn valid_until(&self) -> u64 {
self.valid_until
}
pub fn remaining_at(&self, unix_seconds: u64) -> u64 {
self.valid_until.saturating_sub(unix_seconds)
}
}
impl fmt::Debug for WatchTotpRecord {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("WatchTotpRecord")
.field("path", &self.path)
.field("issuer", &self.issuer)
.field("account", &self.account)
.field("code", &"[REDACTED]")
.field("period", &self.period)
.field("valid_until", &self.valid_until)
.finish()
}
}
#[derive(Default)]
pub struct WatchRuntime {
receiver: WatchSnapshotReceiver,
protected_data_available: bool,
}
impl WatchRuntime {
pub fn apply_snapshot(
&mut self,
bytes: Vec<u8>,
) -> Result<WatchSnapshotUpdate, WatchSnapshotError> {
let apply = self.receiver.apply(SecretBytes::new(bytes))?;
self.protected_data_available = true;
let persistence = match apply {
WatchSnapshotApply::Replaced | WatchSnapshotApply::PairingChanged => {
WatchPersistenceAction::Replace
}
WatchSnapshotApply::Revoked => WatchPersistenceAction::Delete,
WatchSnapshotApply::Duplicate | WatchSnapshotApply::Stale => {
WatchPersistenceAction::Keep
}
};
let current = self.receiver.current();
Ok(WatchSnapshotUpdate {
apply,
persistence,
revision: current.map(WatchSnapshot::revision),
selected_entries: current
.map(|snapshot| u32::try_from(snapshot.entries().len()).unwrap_or(u32::MAX))
.unwrap_or(0),
receipt: self.receiver.current_receipt().unwrap_or_default(),
})
}
pub fn records_at(
&self,
unix_seconds: u64,
) -> Result<Vec<WatchTotpRecord>, WatchSnapshotError> {
if !self.protected_data_available {
return Err(WatchSnapshotError::ProtectedDataUnavailable);
}
let Some(snapshot) = self.receiver.current() else {
return Ok(Vec::new());
};
snapshot
.entries()
.iter()
.map(|entry| {
let counter = unix_seconds / entry.period;
let valid_until = counter
.checked_add(1)
.and_then(|counter| counter.checked_mul(entry.period))
.ok_or(WatchSnapshotError::InvalidEntry)?;
let code = otp_core::code_for_counter(
entry.algorithm,
entry.secret.expose(),
entry.digits,
counter,
)
.map_err(|_| WatchSnapshotError::InvalidEntry)?;
Ok(WatchTotpRecord {
path: entry.path.clone(),
issuer: entry.issuer.clone(),
account: entry.account.clone(),
code,
period: entry.period,
valid_until,
})
})
.collect()
}
pub fn protected_data_unavailable(&mut self) {
self.receiver.clear_secrets();
self.protected_data_available = false;
}
pub fn no_persisted_snapshot(&mut self) {
self.receiver.revoke();
self.protected_data_available = true;
}
}
#[cfg(feature = "full")]
pub struct WatchSnapshotSender { pub struct WatchSnapshotSender {
path: PathBuf, path: PathBuf,
journal: SenderJournal, journal: SenderJournal,
status: MobileWatchSnapshotStatus, status: MobileWatchSnapshotStatus,
} }
#[cfg(feature = "full")]
impl WatchSnapshotSender { impl WatchSnapshotSender {
pub fn load(path: PathBuf) -> Self { pub fn load(path: PathBuf) -> Self {
let journal = load_journal(&path).unwrap_or_default(); let journal = load_journal(&path).unwrap_or_default();
@@ -436,6 +613,7 @@ impl WatchSnapshotSender {
} }
} }
#[cfg(feature = "full")]
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
struct SenderJournal { struct SenderJournal {
@@ -455,6 +633,7 @@ struct SenderJournal {
current_revision: Option<u64>, current_revision: Option<u64>,
} }
#[cfg(feature = "full")]
impl Default for SenderJournal { impl Default for SenderJournal {
fn default() -> Self { fn default() -> Self {
Self { Self {
@@ -469,6 +648,7 @@ impl Default for SenderJournal {
} }
} }
#[cfg(feature = "full")]
fn journal_version() -> u32 { fn journal_version() -> u32 {
JOURNAL_VERSION JOURNAL_VERSION
} }
@@ -476,10 +656,12 @@ fn journal_version() -> u32 {
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[repr(u8)] #[repr(u8)]
enum ReceiptKind { enum ReceiptKind {
#[cfg(feature = "full")]
Delivered = 1, Delivered = 1,
Current = 2, Current = 2,
} }
#[cfg(feature = "full")]
struct Receipt { struct Receipt {
kind: ReceiptKind, kind: ReceiptKind,
pairing: [u8; 32], pairing: [u8; 32],
@@ -487,6 +669,7 @@ struct Receipt {
_digest: [u8; 32], _digest: [u8; 32],
} }
#[cfg(feature = "full")]
fn encode_entries(entries: &[WatchSnapshotEntry]) -> Result<Vec<u8>, WatchSnapshotError> { fn encode_entries(entries: &[WatchSnapshotEntry]) -> Result<Vec<u8>, WatchSnapshotError> {
if entries.len() > MAX_ENTRIES { if entries.len() > MAX_ENTRIES {
return Err(WatchSnapshotError::TooManyEntries); return Err(WatchSnapshotError::TooManyEntries);
@@ -518,6 +701,7 @@ fn encode_entries(entries: &[WatchSnapshotEntry]) -> Result<Vec<u8>, WatchSnapsh
Ok(output) Ok(output)
} }
#[cfg(feature = "full")]
fn encode_snapshot( fn encode_snapshot(
pairing: [u8; 32], pairing: [u8; 32],
revision: u64, revision: u64,
@@ -563,8 +747,10 @@ fn decode_snapshot(bytes: &[u8]) -> Result<WatchSnapshot, WatchSnapshotError> {
} }
let mut entries = Vec::with_capacity(count); let mut entries = Vec::with_capacity(count);
for _ in 0..count { for _ in 0..count {
let path = EntryPath::parse(&take_text(&mut input)?) let path = take_text(&mut input)?;
.map_err(|_| WatchSnapshotError::InvalidEntry)?; if !valid_entry_path(&path) {
return Err(WatchSnapshotError::InvalidEntry);
}
let issuer = match take_u8(&mut input)? { let issuer = match take_u8(&mut input)? {
0 => None, 0 => None,
1 => Some(take_text(&mut input)?), 1 => Some(take_text(&mut input)?),
@@ -631,6 +817,7 @@ fn encode_receipt(
output output
} }
#[cfg(feature = "full")]
fn decode_receipt(bytes: &[u8]) -> Result<Receipt, WatchSnapshotError> { fn decode_receipt(bytes: &[u8]) -> Result<Receipt, WatchSnapshotError> {
if bytes.len() != 4 + 2 + 1 + 32 + 8 + 32 + 32 { if bytes.len() != 4 + 2 + 1 + 32 + 8 + 32 + 32 {
return Err(WatchSnapshotError::InvalidReceipt); return Err(WatchSnapshotError::InvalidReceipt);
@@ -663,6 +850,7 @@ fn decode_receipt(bytes: &[u8]) -> Result<Receipt, WatchSnapshotError> {
}) })
} }
#[cfg(feature = "full")]
fn status_from_journal(journal: &SenderJournal) -> MobileWatchSnapshotStatus { fn status_from_journal(journal: &SenderJournal) -> MobileWatchSnapshotStatus {
let (state, revision, detail) = if let Some(revision) = journal.current_revision { let (state, revision, detail) = if let Some(revision) = journal.current_revision {
( (
@@ -699,6 +887,7 @@ fn status_from_journal(journal: &SenderJournal) -> MobileWatchSnapshotStatus {
} }
} }
#[cfg(feature = "full")]
fn snapshot_detail(state: &str, count: u32, revision: u64) -> String { fn snapshot_detail(state: &str, count: u32, revision: u64) -> String {
format!( format!(
"Snapshot revision {revision} with {count} selected TOTP {} is {state}.", "Snapshot revision {revision} with {count} selected TOTP {} is {state}.",
@@ -706,6 +895,7 @@ fn snapshot_detail(state: &str, count: u32, revision: u64) -> String {
) )
} }
#[cfg(feature = "full")]
fn load_journal(path: &Path) -> Option<SenderJournal> { fn load_journal(path: &Path) -> Option<SenderJournal> {
let metadata = fs::symlink_metadata(path).ok()?; let metadata = fs::symlink_metadata(path).ok()?;
if metadata.file_type().is_symlink() || !metadata.is_file() { if metadata.file_type().is_symlink() || !metadata.is_file() {
@@ -715,6 +905,7 @@ fn load_journal(path: &Path) -> Option<SenderJournal> {
(journal.version == JOURNAL_VERSION).then_some(journal) (journal.version == JOURNAL_VERSION).then_some(journal)
} }
#[cfg(feature = "full")]
fn save_journal(path: &Path, journal: &SenderJournal) -> Result<(), WatchSnapshotError> { fn save_journal(path: &Path, journal: &SenderJournal) -> Result<(), WatchSnapshotError> {
let parent = path.parent().ok_or(WatchSnapshotError::JournalWrite)?; let parent = path.parent().ok_or(WatchSnapshotError::JournalWrite)?;
fs::create_dir_all(parent).map_err(|_| WatchSnapshotError::JournalWrite)?; fs::create_dir_all(parent).map_err(|_| WatchSnapshotError::JournalWrite)?;
@@ -737,7 +928,7 @@ fn save_journal(path: &Path, journal: &SenderJournal) -> Result<(), WatchSnapsho
.map_err(|_| WatchSnapshotError::JournalWrite) .map_err(|_| WatchSnapshotError::JournalWrite)
} }
#[cfg(unix)] #[cfg(all(feature = "full", unix))]
fn set_private_permissions(temporary: &TempFile<'_>) -> Result<(), WatchSnapshotError> { fn set_private_permissions(temporary: &TempFile<'_>) -> Result<(), WatchSnapshotError> {
use cap_std::fs::{Permissions, PermissionsExt as _}; use cap_std::fs::{Permissions, PermissionsExt as _};
temporary temporary
@@ -746,7 +937,7 @@ fn set_private_permissions(temporary: &TempFile<'_>) -> Result<(), WatchSnapshot
.map_err(|_| WatchSnapshotError::JournalWrite) .map_err(|_| WatchSnapshotError::JournalWrite)
} }
#[cfg(not(unix))] #[cfg(all(feature = "full", not(unix)))]
fn set_private_permissions(_temporary: &TempFile<'_>) -> Result<(), WatchSnapshotError> { fn set_private_permissions(_temporary: &TempFile<'_>) -> Result<(), WatchSnapshotError> {
Ok(()) Ok(())
} }
@@ -754,6 +945,7 @@ fn set_private_permissions(_temporary: &TempFile<'_>) -> Result<(), WatchSnapsho
fn digest(bytes: &[u8]) -> [u8; 32] { fn digest(bytes: &[u8]) -> [u8; 32] {
Sha256::digest(bytes).into() Sha256::digest(bytes).into()
} }
#[cfg(feature = "full")]
fn decode_hash(text: &str) -> Result<[u8; 32], WatchSnapshotError> { fn decode_hash(text: &str) -> Result<[u8; 32], WatchSnapshotError> {
let bytes = HEXLOWER_PERMISSIVE let bytes = HEXLOWER_PERMISSIVE
.decode(text.as_bytes()) .decode(text.as_bytes())
@@ -762,15 +954,18 @@ fn decode_hash(text: &str) -> Result<[u8; 32], WatchSnapshotError> {
.try_into() .try_into()
.map_err(|_| WatchSnapshotError::InvalidJournal) .map_err(|_| WatchSnapshotError::InvalidJournal)
} }
#[cfg(feature = "full")]
fn put_u32(output: &mut Vec<u8>, value: u32) { fn put_u32(output: &mut Vec<u8>, value: u32) {
output.extend_from_slice(&value.to_be_bytes()); output.extend_from_slice(&value.to_be_bytes());
} }
fn put_u64(output: &mut Vec<u8>, value: u64) { fn put_u64(output: &mut Vec<u8>, value: u64) {
output.extend_from_slice(&value.to_be_bytes()); output.extend_from_slice(&value.to_be_bytes());
} }
#[cfg(feature = "full")]
fn put_text(output: &mut Vec<u8>, value: &str) -> Result<(), WatchSnapshotError> { fn put_text(output: &mut Vec<u8>, value: &str) -> Result<(), WatchSnapshotError> {
put_bytes(output, value.as_bytes(), MAX_TEXT_BYTES) put_bytes(output, value.as_bytes(), MAX_TEXT_BYTES)
} }
#[cfg(feature = "full")]
fn put_bytes(output: &mut Vec<u8>, value: &[u8], max: usize) -> Result<(), WatchSnapshotError> { fn put_bytes(output: &mut Vec<u8>, value: &[u8], max: usize) -> Result<(), WatchSnapshotError> {
if value.len() > max { if value.len() > max {
return Err(WatchSnapshotError::InvalidEntry); return Err(WatchSnapshotError::InvalidEntry);
@@ -826,6 +1021,16 @@ fn take_text(input: &mut &[u8]) -> Result<String, WatchSnapshotError> {
.map_err(|_| WatchSnapshotError::InvalidEntry) .map_err(|_| WatchSnapshotError::InvalidEntry)
} }
fn valid_entry_path(value: &str) -> bool {
let path = Path::new(value);
!value.is_empty()
&& !value.ends_with('/')
&& !path.is_absolute()
&& path
.components()
.all(|component| matches!(component, std::path::Component::Normal(_)))
}
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
pub enum WatchSnapshotError { pub enum WatchSnapshotError {
InvalidPairing, InvalidPairing,
@@ -841,6 +1046,7 @@ pub enum WatchSnapshotError {
NoPendingSnapshot, NoPendingSnapshot,
InvalidJournal, InvalidJournal,
JournalWrite, JournalWrite,
ProtectedDataUnavailable,
} }
impl fmt::Display for WatchSnapshotError { impl fmt::Display for WatchSnapshotError {
@@ -861,6 +1067,7 @@ impl fmt::Display for WatchSnapshotError {
Self::NoPendingSnapshot => "there is no pending Apple Watch snapshot", Self::NoPendingSnapshot => "there is no pending Apple Watch snapshot",
Self::InvalidJournal => "the Apple Watch synchronization journal is invalid", Self::InvalidJournal => "the Apple Watch synchronization journal is invalid",
Self::JournalWrite => "the Apple Watch synchronization journal could not be saved", Self::JournalWrite => "the Apple Watch synchronization journal could not be saved",
Self::ProtectedDataUnavailable => "protected Apple Watch data is unavailable",
}) })
} }
} }

View File

@@ -3,15 +3,13 @@
use std::{error::Error, fmt, ops::Range, str, sync::Mutex}; use std::{error::Error, fmt, ops::Range, str, sync::Mutex};
use data_encoding::BASE32_NOPAD; use data_encoding::BASE32_NOPAD;
use hmac::Hmac;
use sha1::Sha1;
use sha2::{Sha256, Sha512};
use zeroize::Zeroize as _; use zeroize::Zeroize as _;
use crate::{ use crate::{
command::{OtpAppendRequest, OtpInputSource, OtpInsertRequest}, command::{OtpAppendRequest, OtpInputSource, OtpInsertRequest},
crypto::{CryptoError, KeyStore, SecretProvider}, crypto::{CryptoError, KeyStore, SecretProvider},
git::{AutomaticEntryCommitter, GitError, GitIdentity}, git::{AutomaticEntryCommitter, GitError, GitIdentity},
otp_core,
recipient::{RecipientPolicyError, RecipientPolicyManager, SigningPolicy}, recipient::{RecipientPolicyError, RecipientPolicyManager, SigningPolicy},
repository::{EncryptedEntry, EntryPath, Repository, RepositoryError, SecretBytes}, repository::{EncryptedEntry, EntryPath, Repository, RepositoryError, SecretBytes},
write::{EntryAction, EntryCommit, EntryCommitError, EntryCommitter, OverwriteDecision}, write::{EntryAction, EntryCommit, EntryCommitError, EntryCommitter, OverwriteDecision},
@@ -27,12 +25,7 @@ pub enum OtpKind {
Hotp, Hotp,
} }
#[derive(Clone, Copy, Debug, Eq, PartialEq)] pub use crate::otp_core::OtpAlgorithm;
pub enum OtpAlgorithm {
Sha1,
Sha256,
Sha512,
}
/// A validated key URI whose encoded and decoded secrets zeroize on drop. /// A validated key URI whose encoded and decoded secrets zeroize on drop.
pub struct OtpUri { pub struct OtpUri {
@@ -251,21 +244,8 @@ impl OtpUri {
} }
pub fn code_for_counter(&self, counter: u64) -> Result<SecretBytes, OtpError> { pub fn code_for_counter(&self, counter: u64) -> Result<SecretBytes, OtpError> {
let message = counter.to_be_bytes(); otp_core::code_for_counter(self.algorithm, self.secret.expose(), self.digits, counter)
let mut digest = match self.algorithm { .map_err(|_| OtpError::InvalidSecret)
OtpAlgorithm::Sha1 => hmac_digest::<Hmac<Sha1>>(self.secret.expose(), &message)?,
OtpAlgorithm::Sha256 => hmac_digest::<Hmac<Sha256>>(self.secret.expose(), &message)?,
OtpAlgorithm::Sha512 => hmac_digest::<Hmac<Sha512>>(self.secret.expose(), &message)?,
};
let offset = usize::from(digest[digest.len() - 1] & 0x0f);
let binary = (u32::from(digest[offset]) & 0x7f) << 24
| u32::from(digest[offset + 1]) << 16
| u32::from(digest[offset + 2]) << 8
| u32::from(digest[offset + 3]);
digest.zeroize();
let modulus = 10_u32.pow(self.digits);
let code = format!("{:0width$}", binary % modulus, width = self.digits as usize);
Ok(SecretBytes::new(code.into_bytes()))
} }
fn incremented_hotp(&self) -> Result<(u64, Self), OtpError> { fn incremented_hotp(&self) -> Result<(u64, Self), OtpError> {
@@ -1204,19 +1184,6 @@ fn percent_encode(value: &str) -> String {
encoded encoded
} }
fn hmac_digest<M>(key: &[u8], message: &[u8]) -> Result<Vec<u8>, OtpError>
where
M: hmac::digest::Mac + hmac::digest::KeyInit,
{
let mut mac =
<M as hmac::digest::Mac>::new_from_slice(key).map_err(|_| OtpError::InvalidSecret)?;
mac.update(message);
let mut output = mac.finalize().into_bytes();
let digest = output.to_vec();
output.fill(0);
Ok(digest)
}
pub(crate) fn find_uri( pub(crate) fn find_uri(
plaintext: &SecretBytes, plaintext: &SecretBytes,
entry: &EntryPath, entry: &EntryPath,

View File

@@ -0,0 +1,97 @@
use std::{error::Error, fmt};
use hmac::Hmac;
use sha1::Sha1;
use sha2::{Sha256, Sha512};
use zeroize::Zeroize as _;
use crate::secret::SecretBytes;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum OtpAlgorithm {
Sha1,
Sha256,
Sha512,
}
pub fn code_for_counter(
algorithm: OtpAlgorithm,
secret: &[u8],
digits: u32,
counter: u64,
) -> Result<SecretBytes, OtpCoreError> {
if secret.is_empty() || !matches!(digits, 6 | 8) {
return Err(OtpCoreError);
}
let message = counter.to_be_bytes();
let mut digest = match algorithm {
OtpAlgorithm::Sha1 => hmac_digest::<Hmac<Sha1>>(secret, &message)?,
OtpAlgorithm::Sha256 => hmac_digest::<Hmac<Sha256>>(secret, &message)?,
OtpAlgorithm::Sha512 => hmac_digest::<Hmac<Sha512>>(secret, &message)?,
};
let offset = usize::from(digest[digest.len() - 1] & 0x0f);
let binary = (u32::from(digest[offset]) & 0x7f) << 24
| u32::from(digest[offset + 1]) << 16
| u32::from(digest[offset + 2]) << 8
| u32::from(digest[offset + 3]);
digest.zeroize();
let code = format!(
"{:0width$}",
binary % 10_u32.pow(digits),
width = digits as usize
);
Ok(SecretBytes::new(code.into_bytes()))
}
fn hmac_digest<M>(key: &[u8], message: &[u8]) -> Result<Vec<u8>, OtpCoreError>
where
M: hmac::digest::Mac + hmac::digest::KeyInit,
{
let mut mac = <M as hmac::digest::Mac>::new_from_slice(key).map_err(|_| OtpCoreError)?;
mac.update(message);
let mut output = mac.finalize().into_bytes();
let digest = output.to_vec();
output.fill(0);
Ok(digest)
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct OtpCoreError;
impl fmt::Display for OtpCoreError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("OTP parameters are invalid")
}
}
impl Error for OtpCoreError {}
#[cfg(test)]
mod tests {
use super::{OtpAlgorithm, code_for_counter};
#[test]
fn rfc_6238_vectors_cover_every_watch_algorithm() {
let vectors = [
(
OtpAlgorithm::Sha1,
b"12345678901234567890".as_slice(),
"94287082",
),
(
OtpAlgorithm::Sha256,
b"12345678901234567890123456789012".as_slice(),
"46119246",
),
(
OtpAlgorithm::Sha512,
b"1234567890123456789012345678901234567890123456789012345678901234".as_slice(),
"90693936",
),
];
for (algorithm, secret, expected) in vectors {
let code = code_for_counter(algorithm, secret, 8, 59 / 30).expect("RFC vector");
assert_eq!(code.expose(), expected.as_bytes());
}
}
}

View File

@@ -12,9 +12,9 @@ use std::{
#[cfg(test)] #[cfg(test)]
use std::collections::BTreeSet; use std::collections::BTreeSet;
pub use crate::secret::SecretBytes;
use cap_std::{ambient_authority, fs::Dir}; use cap_std::{ambient_authority, fs::Dir};
use cap_tempfile::TempFile; use cap_tempfile::TempFile;
use zeroize::Zeroize;
const ENTRY_EXTENSION: &str = "gpg"; const ENTRY_EXTENSION: &str = "gpg";
const RECIPIENT_FILE: &str = ".gpg-id"; const RECIPIENT_FILE: &str = ".gpg-id";
@@ -132,35 +132,6 @@ impl fmt::Debug for EncryptedEntry {
} }
} }
/// Decrypted bytes that are redacted in diagnostics and zeroed when dropped.
pub struct SecretBytes(Vec<u8>);
impl SecretBytes {
pub fn new(bytes: Vec<u8>) -> Self {
Self(bytes)
}
pub fn expose(&self) -> &[u8] {
&self.0
}
pub fn expose_mut(&mut self) -> &mut [u8] {
&mut self.0
}
}
impl fmt::Debug for SecretBytes {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("SecretBytes([REDACTED])")
}
}
impl Drop for SecretBytes {
fn drop(&mut self) {
self.0.zeroize();
}
}
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
pub struct EntryRecord { pub struct EntryRecord {
path: EntryPath, path: EntryPath,

View File

@@ -0,0 +1,32 @@
use std::fmt;
use zeroize::Zeroize as _;
/// Secret bytes that are redacted in diagnostics and zeroed when dropped.
pub struct SecretBytes(Vec<u8>);
impl SecretBytes {
pub fn new(bytes: Vec<u8>) -> Self {
Self(bytes)
}
pub fn expose(&self) -> &[u8] {
&self.0
}
pub fn expose_mut(&mut self) -> &mut [u8] {
&mut self.0
}
}
impl fmt::Debug for SecretBytes {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("SecretBytes([REDACTED])")
}
}
impl Drop for SecretBytes {
fn drop(&mut self) {
self.0.zeroize();
}
}

View File

@@ -4,8 +4,8 @@ use std::fs;
use ironstorage::{ use ironstorage::{
mobile_watch::{ mobile_watch::{
MobileWatchSnapshotState, WatchSnapshotApply, WatchSnapshotEntry, WatchSnapshotReceiver, MobileWatchSnapshotState, WatchPersistenceAction, WatchRuntime, WatchSnapshotApply,
WatchSnapshotSender, WatchSnapshotEntry, WatchSnapshotReceiver, WatchSnapshotSender,
}, },
otp::OtpAlgorithm, otp::OtpAlgorithm,
repository::{EntryPath, SecretBytes}, repository::{EntryPath, SecretBytes},
@@ -15,7 +15,7 @@ type TestResult = Result<(), Box<dyn std::error::Error>>;
fn entry(path: &str, issuer: &str, account: &str, secret: &[u8]) -> WatchSnapshotEntry { fn entry(path: &str, issuer: &str, account: &str, secret: &[u8]) -> WatchSnapshotEntry {
WatchSnapshotEntry::new( WatchSnapshotEntry::new(
EntryPath::parse(path).expect("fixture path"), EntryPath::parse(path).expect("fixture path").to_string(),
Some(issuer.to_owned()), Some(issuer.to_owned()),
account.to_owned(), account.to_owned(),
OtpAlgorithm::Sha256, OtpAlgorithm::Sha256,
@@ -165,3 +165,49 @@ fn journal_keeps_revisions_monotonic_across_sender_reloads() -> TestResult {
assert_eq!(changed.revision(), 2); assert_eq!(changed.revision(), 2);
Ok(()) Ok(())
} }
#[test]
fn watch_runtime_generates_view_ready_totp_and_clears_secrets_when_locked() -> TestResult {
let directory = tempfile::tempdir()?;
let mut sender = WatchSnapshotSender::load(directory.path().join("watch-snapshot.toml"));
let snapshot = sender.prepare(
"paired-watch",
vec![WatchSnapshotEntry::new(
"otp/alice".to_owned(),
Some("Acme".to_owned()),
"alice".to_owned(),
OtpAlgorithm::Sha1,
8,
30,
SecretBytes::new(b"12345678901234567890".to_vec()),
)],
)?;
let mut runtime = WatchRuntime::default();
let update = runtime.apply_snapshot(snapshot.snapshot().expose().to_vec())?;
assert_eq!(update.apply(), WatchSnapshotApply::Replaced);
assert_eq!(update.persistence(), WatchPersistenceAction::Replace);
assert_eq!(update.selected_entries(), 1);
assert!(!update.receipt().is_empty());
let records = runtime.records_at(59)?;
assert_eq!(records.len(), 1);
assert_eq!(records[0].path(), "otp/alice");
assert_eq!(records[0].code().expose(), b"94287082");
assert_eq!(records[0].valid_until(), 60);
assert_eq!(records[0].remaining_at(59), 1);
runtime.protected_data_unavailable();
assert!(runtime.records_at(59).is_err());
let restored = runtime.apply_snapshot(snapshot.snapshot().expose().to_vec())?;
assert_eq!(restored.persistence(), WatchPersistenceAction::Replace);
assert_eq!(runtime.records_at(59)?[0].code().expose(), b"94287082");
let revocation = sender.prepare("paired-watch", Vec::new())?;
let revoked = runtime.apply_snapshot(revocation.snapshot().expose().to_vec())?;
assert_eq!(revoked.apply(), WatchSnapshotApply::Revoked);
assert_eq!(revoked.persistence(), WatchPersistenceAction::Delete);
assert!(runtime.records_at(59)?.is_empty());
Ok(())
}

View File

@@ -0,0 +1,15 @@
[package]
name = "ironstorage-watch-apple"
description = "Minimal UniFFI boundary for the IronStorage watchOS TOTP core"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
publish = false
[lib]
name = "ironstorage_watch"
crate-type = ["lib", "staticlib", "cdylib"]
[dependencies]
ironstorage = { path = "../storage", default-features = false, features = ["watch"] }
uniffi.workspace = true

View File

@@ -0,0 +1,190 @@
#![forbid(unsafe_code)]
#![deny(clippy::disallowed_types)]
//! Mechanical UniFFI exports for the minimal watchOS Rust runtime.
use std::{
error::Error,
fmt,
sync::{Arc, Mutex},
};
use ironstorage::mobile_watch::{
WatchPersistenceAction as StoragePersistenceAction, WatchRuntime as StorageWatchRuntime,
WatchSnapshotApply as StorageSnapshotApply, WatchSnapshotError as StorageWatchError,
WatchSnapshotUpdate as StorageSnapshotUpdate, WatchTotpRecord as StorageTotpRecord,
};
uniffi::setup_scaffolding!();
#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
pub enum WatchSnapshotApply {
Replaced,
Revoked,
Duplicate,
Stale,
PairingChanged,
}
impl From<StorageSnapshotApply> for WatchSnapshotApply {
fn from(value: StorageSnapshotApply) -> Self {
match value {
StorageSnapshotApply::Replaced => Self::Replaced,
StorageSnapshotApply::Revoked => Self::Revoked,
StorageSnapshotApply::Duplicate => Self::Duplicate,
StorageSnapshotApply::Stale => Self::Stale,
StorageSnapshotApply::PairingChanged => Self::PairingChanged,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
pub enum WatchPersistenceAction {
Keep,
Replace,
Delete,
}
impl From<StoragePersistenceAction> for WatchPersistenceAction {
fn from(value: StoragePersistenceAction) -> Self {
match value {
StoragePersistenceAction::Keep => Self::Keep,
StoragePersistenceAction::Replace => Self::Replace,
StoragePersistenceAction::Delete => Self::Delete,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
pub struct WatchSnapshotUpdate {
pub apply: WatchSnapshotApply,
pub persistence: WatchPersistenceAction,
pub revision: Option<u64>,
pub selected_entries: u32,
pub receipt: Vec<u8>,
}
impl From<StorageSnapshotUpdate> for WatchSnapshotUpdate {
fn from(value: StorageSnapshotUpdate) -> Self {
Self {
apply: value.apply().into(),
persistence: value.persistence().into(),
revision: value.revision(),
selected_entries: value.selected_entries(),
receipt: value.receipt().to_vec(),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
pub struct WatchTotpRecord {
pub path: String,
pub issuer: Option<String>,
pub account: String,
pub code: String,
pub period: u64,
pub valid_until: u64,
}
impl From<StorageTotpRecord> for WatchTotpRecord {
fn from(value: StorageTotpRecord) -> Self {
Self {
path: value.path().to_owned(),
issuer: value.issuer().map(str::to_owned),
account: value.account().to_owned(),
code: String::from_utf8(value.code().expose().to_vec())
.expect("storage-generated TOTP codes are ASCII"),
period: value.period(),
valid_until: value.valid_until(),
}
}
}
#[derive(Debug, uniffi::Error)]
pub enum WatchFfiError {
Failed { message: String },
}
impl fmt::Display for WatchFfiError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Failed { message } => formatter.write_str(message),
}
}
}
impl Error for WatchFfiError {}
impl From<StorageWatchError> for WatchFfiError {
fn from(value: StorageWatchError) -> Self {
Self::Failed {
message: value.to_string(),
}
}
}
#[derive(uniffi::Object)]
pub struct WatchCore {
runtime: Mutex<StorageWatchRuntime>,
}
#[uniffi::export]
impl WatchCore {
pub fn apply_snapshot(&self, snapshot: Vec<u8>) -> Result<WatchSnapshotUpdate, WatchFfiError> {
self.runtime
.lock()
.map_err(|_| lock_error())?
.apply_snapshot(snapshot)
.map(Into::into)
.map_err(Into::into)
}
pub fn records_at(&self, unix_seconds: u64) -> Result<Vec<WatchTotpRecord>, WatchFfiError> {
self.runtime
.lock()
.map_err(|_| lock_error())?
.records_at(unix_seconds)
.map(|records| records.into_iter().map(Into::into).collect())
.map_err(Into::into)
}
pub fn protected_data_unavailable(&self) -> Result<(), WatchFfiError> {
self.runtime
.lock()
.map_err(|_| lock_error())?
.protected_data_unavailable();
Ok(())
}
pub fn no_persisted_snapshot(&self) -> Result<(), WatchFfiError> {
self.runtime
.lock()
.map_err(|_| lock_error())?
.no_persisted_snapshot();
Ok(())
}
}
fn lock_error() -> WatchFfiError {
WatchFfiError::Failed {
message: "Apple Watch TOTP state is unavailable".to_owned(),
}
}
#[uniffi::export]
pub fn watch_core() -> Arc<WatchCore> {
Arc::new(WatchCore {
runtime: Mutex::new(StorageWatchRuntime::default()),
})
}
#[cfg(test)]
mod tests {
#[test]
fn bridge_masks_records_when_protected_data_is_unavailable() {
let core = super::watch_core();
core.no_persisted_snapshot().expect("available Keychain");
assert!(core.records_at(59).expect("empty snapshot").is_empty());
core.protected_data_unavailable().expect("lock transition");
assert!(core.records_at(59).is_err());
}
}