電波の届きにくい倉庫・訪問診療・工場でも0.1秒で動くモバイル基盤:
〜「カメラで撮るだけ・3秒話すだけ」で完結する Flutter × Rust × 暗号化金庫〜
監修:株式会社オープンナレッジ 代表取締役CTO 加藤 明生(上級医療情報技師 / 登録情報処理安全確保支援士)
この記事の要点と4つの章のご案内
| 章構成 | 主なテーマ | 要点と確認ポイント |
|---|---|---|
| 1. 基本と全体像 | 「1行書くたびに本社へ電話する社員」と「鍵付きポケット手帳を持つ社員」 | はい、0.1秒で普段通りに動きます。一般的なクラウド専用アプリは毎回インターネットの向こう側へ通信するため電波が弱いと止まってしまいますが、当社のモバイル基盤はスマートフォン内部の暗号化金庫(SQLite + SQLCipher)にまず記録し、電波が戻った瞬間に裏側で自動同期する「ローカル・ファースト設計」を採用しているためです。 |
| 2. 現場での進め方と対策 | 電波がなくても止まらない「3層モバイル基盤」の仕組み | 文字打ちはほとんど必要ありません。領収書や点検表を「カメラで撮るだけ」、または作業後に「3秒間声で話すだけ」で、アプリ内のAIとRustエンジンが自動的に整理し、最大3つの大きなボタンから選ぶだけで完了します。 |
| 3. 関連する制度とルール | 手袋をしたままでも、夕方の疲れた目でも「迷わず押せる」優しさの設計 | 個人の写真フォルダ(カメラロール)には一切保存されません。撮影した瞬間にアプリ内部の暗号化領域(AES-256暗号化)へ直接封印されるため、万が一端末を紛失した場合や私有スマホの業務利用(BYOD)でも、経済産業省「SCS評価制度(★3)」の基準を満たす高い安全性を保てます。 |
| 4. 詳しい技術仕様としくみ | 端末内カメラロール分離・SQLCipher暗号化・CRDT同期の検証基準 | 監修:株式会社オープンナレッジ 代表取締役CTO 加藤 明生(上級医療情報技師 / 登録情報処理安全確保支援士) |
「電波ゼロの場所でも0.1秒で手元の暗号化手帳に記録し、電波が戻ったら自動で本社へ届ける。」
現場で使うスマホアプリが「電波が悪くてずっと読み込み中のまま動かない」「細かい文字入力が大変で使われなくなる」という失敗をなくすため、私たちは滑らかな画面(Flutter)・超高速の共通頭脳(Rust)・端末内の暗号化金庫(SQLite暗号化)を1つに統合しました。医療の訪問先、冷凍倉庫、建設現場でもストレスゼロで動きます。
電波がなくても止まらない「3層モバイル基盤」の仕組み
| 比較項目 | 一般的なブラウザ・Webアプリ | iOS / Android 別々の従来開発 | 推奨設計:Flutter × Rust オフライン基盤 |
|---|---|---|---|
| 地下・倉庫・移動中 | 電波が切れるとエラーになり入力内容が消える | オフライン対応の追加開発に高額費用が必要 | 標準で0.1秒オフライン記録+自動再同期 |
| 開発・保守の費用 | 初期費用は安いが操作性が重い | 2つのチームが必要で費用・期間が約2倍 | 1つの共通Rust頭脳でiOS/Android両対応 |
| 端末紛失時の安全性 | ブラウザのキャッシュや写真フォルダに残る危険 | 設計次第でバラつきが出やすい | 端末内AES-256暗号化+カメラロール全体非保存 |
手袋をしたままでも、夕方の疲れた目でも「迷わず押せる」優しさの設計
私たちが現場向けのモバイルアプリを作るとき、一番大切にしているのは「明るいオフィスの机の上」ではなく、「薄暗い倉庫の奥」「雨の日の配送トラックの運転席」「訪問介護を終えた夕方の車内」でどう見えるか、どう触れるかです。
たとえば、軍手や医療用手袋をしたままでも押し間違えないよう、ボタンの大きさは人間工学に基づいた48ピクセル以上のゆとりサイズに設計し、画面の中の入力項目は最大3つ以内に絞っています。
また、当社が研究開発を進める手話・筋電(sEMG)アシスト技術『もしもの話』と同じく、スマートフォンの内部で音声やセンサー情報を瞬時に処理できるため、インターネット回線が混み合う災害時や緊急時にも「普段通りに使える命綱」として機能します。
今日から確認できる「現場スマホ・タブレット活用」3つの安全チェック
※クリックして自社の現場状況をチェックしてみましょう(外部送信されません)
端末内カメラロール分離・SQLCipher暗号化・CRDT同期の検証基準
現場のスマートフォンやタブレットで伝票や本人確認書類を扱う際、個人用アルバムや個人クラウドへの誤同期を防ぎ、圏外でも業務を止めないための技術検証項目です。
| 保護対象 | 実装アーキテクチャ | セキュリティ・可用性保証 |
|---|---|---|
| 撮影画像の隔離保管 | App-Scoped Sandbox + In-Memory Stream | OS標準カメラロールや個人クラウド(iCloud/Googleフォト)へ1バイトも書き出さない |
| オフラインDB暗号化 | SQLCipher AES-256-CBC + OS Keystore | Secure Enclave / StrongBox 内のハードウェア保護鍵で4KBページ単位に暗号化 |
| 圏外・再接続時の自動統合 | HLC (Hybrid Logical Clock) LWW-CRDT | 地下や倉庫の圏外環境でも0.1秒でローカル保存し、電波回復時に競合なく差分同期 |
私たちがスマートフォン内部の暗号化金庫として採用しているデータベース「SQLite(エスキューライト)」は、世界で最も多く使われているソフトウェアの一つで、エアバス社の最新旅客機(A350)のフライトソフトにも採用されています。その理由は、開発チームが「プログラム本体よりも1,000倍以上多いテストコード(数億通りの過酷な故障テスト)」をパスさせてから公開しているため、高度1万メートルのオフライン環境でも決して壊れないからです。
ご質問・公的制度の活用に関するお問い合わせ窓口
本ページの解説内容に関するご質問や、経済産業省「サプライチェーン強化に向けたセキュリティ対策評価制度(SCS評価制度)」・IPA「SECURITY ACTION」等の公的制度活用、地域の防犯・教育講習に関するお問い合わせは、以下の窓口にて承っております。
圏外・災害現場でも原則としてデータが衝突しない Hybrid Logical Clocks (HLC) と状態ベース CvRDT(半束合流)の数理証明
医療訪問看護・冷凍倉庫検品・災害避難所運営などのエッジ現場では、地下や山間部でのモバイル通信途絶(Network Partition)が日常的に発生します。CAP定理(Gilbert & Lynch, 2002)により、分断耐性(Partition Tolerance)の下で即時応答性(Availability: 0ms ローカル書き込み)を保証するには、結果整合性モデルを採用しつつ、再接続時の更新競合(Write-Write Conflict)を人間の手動マージなしで数学的に収束させる必要があります。
当社のモバイルコア(Rust 2024 FFI ライブラリ + SQLite WAL モード)は、物理時計(NTP)のドリフトや逆行に耐性を持つ Hybrid Logical Clock(HLC: Kulkarni et al., 2014) を64ビット整数(上位48-bit 物理ミリ秒 l.j、下位16-bit 論理カウンタ c.j)へパッキングして全レコードに付与します。イベント e → f(Lamport 因果先行関係)に対し、物理時計が多少ずれていても必ず (l.e, c.e, nodee) < (l.f, c.f, nodef) の全順序(Total Order)が保証されます。
さらに各データ構造を結び半束(Join-Semilattice \langle S, \sqcup \rangle)をなす状態ベース CRDT(CvRDT)として定義し、マージ演算子 \sqcup が 01 交換律(A \sqcup B = B \sqcup A)、02 結合律((A \sqcup B) \sqcup C = A \sqcup (B \sqcup C))、03 べき等律(A \sqcup A = A)の3公理を満たすことをコンパイル時テストで検証しています。これにより、パケットの再送・順序入れ替わり・複数端末の同時オフライン編集が起きても、全端末の状態は1ビットの狂いもなく同一の最小上界(Least Upper Bound: \sup(A, B))へ収束します。
| Algebraic Property / Bitfield | Mathematical Definition | 64-Bit / Memory Representation | Field Operational Guarantee |
|---|---|---|---|
HLC Physical l | l' = max(l, ptwall, lmsg) | Bits 63..16 (48-bit Unix ms) | Monotonic even if device clock skews backward |
HLC Logical c | Incremented when l' == l | Bits 15..0 (16-bit counter, 65535/ms) | Captures sub-millisecond causal ordering |
| Commutativity | s1 \sqcup s2 = s2 \sqcup s1 | Lexicographic (HLC, NodeId) tie-break | Order of sync packet arrival never matters |
| Idempotence | s1 \sqcup s1 = s1 | Pure functional state transitions | Safe retry over lossy 4G/5G/Wi-Fi links |
#![no_std]
#![forbid(unsafe_code)]
/// 64-bit packed Hybrid Logical Clock: [48-bit physical_ms | 16-bit logical_counter]
#[repr(transparent)]
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct PackedHlc64(pub u64);
impl PackedHlc64 {
#[inline]
pub const fn new(physical_ms: u64, logical: u16) -> Self {
Self(((physical_ms & 0xFFFF_FFFF_FFFF) << 16) | (logical as u64))
}
#[inline]
pub const fn physical_ms(self) -> u64 {
self.0 >> 16
}
#[inline]
pub const fn logical(self) -> u16 {
(self.0 & 0xFFFF) as u16
}
/// Local event tick: advances monotonically even if wall clock regresses.
pub const fn tick_local(self, wall_ms: u64) -> Self {
let old_l = self.physical_ms();
if wall_ms > old_l {
Self::new(wall_ms, 0)
} else {
Self::new(old_l, self.logical().saturating_add(1))
}
}
/// Receive remote message HLC: enforces causal ordering e -> f => HLC(e) < HLC(f).
pub const fn update_remote(self, remote: Self, wall_ms: u64) -> Self {
let l_self = self.physical_ms();
let l_rem = remote.physical_ms();
let max_l = if wall_ms >= l_self && wall_ms >= l_rem {
wall_ms
} else if l_self >= l_rem {
l_self
} else {
l_rem
};
let next_c = if max_l == l_self && max_l == l_rem {
let m = if self.logical() >= remote.logical() { self.logical() } else { remote.logical() };
m.saturating_add(1)
} else if max_l == l_self {
self.logical().saturating_add(1)
} else if max_l == l_rem {
remote.logical().saturating_add(1)
} else {
0
};
Self::new(max_l, next_c)
}
}
/// Join-Semilattice LWW-Register CRDT with deterministic (HLC, NodeId) total order.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct LwwRegister<T: Copy + PartialEq + Eq> {
pub value: T,
pub hlc: PackedHlc64,
pub node_id: u32,
}
impl<T: Copy + PartialEq + Eq> LwwRegister<T> {
/// Least Upper Bound (Join operator `sqcup`): associative, commutative, idempotent.
#[inline]
pub const fn merge(self, other: Self) -> Self {
if self.hlc.0 > other.hlc.0 {
self
} else if self.hlc.0 < other.hlc.0 {
other
} else if self.node_id >= other.node_id {
self
} else {
other
}
}
}