島根県安来市のフリーランスエンジニア_プログラマー画像1
ステルス録音アプリの作り方|完全偽装×高音質録音の実装ガイド – Eatransform

ステルス録音アプリの作り方|完全偽装×高音質録音の実装ガイド

🚧 デモシステム

🔒 StealthRec

完璧なステルス性 – 誰にも気づかれない録音アプリ

⚠️ 重要: このアプリは自己防衛・証拠保全を目的としています。違法な盗聴には使用しないでください。自分が会話の当事者である場合の録音は合法です。

偽装モードを選択
🔢
電卓モード
🕐
時計モード
📝
メモモード
🔴 REC
0
00:00:00
2024年11月30日
メモ
タップして録音開始
📚 実装ガイド

ステルス録音アプリの構築手順

完璧な偽装 × 高音質録音 × 自動クラウドバックアップ

1
開発環境とアーキテクチャ設計

ネイティブアプリで最高のステルス性を実現

  • 開発言語: Swift(iOS)、Kotlin(Android)
  • 音声録音: AVAudioRecorder(iOS)、MediaRecorder(Android)
  • バックグラウンド動作: Background Modes設定
  • ストレージ: Firebase Storage、AWS S3
  • データベース: Realm、Core Data(ローカル)
  • 暗号化: AES-256で録音ファイルを暗号化
🛠️ 推奨技術スタック
Swift(iOS) Kotlin(Android) AVAudioRecorder Firebase Storage Realm Database AES-256暗号化
📦 iOS開発環境構築
# Xcodeでプロジェクト作成
# File > New > Project > App

# Podfileに必要なライブラリを追加
platform :ios, '14.0'
use_frameworks!

target 'StealthRec' do
  pod 'RealmSwift'
  pod 'Firebase/Storage'
  pod 'CryptoSwift'
end

# ライブラリインストール
pod install
2
偽装UI(電卓・時計・メモ)の実装

完璧に本物に見える偽装画面を作成

  • 電卓モード: 実際に計算できる本物の電卓として機能
  • 時計モード: リアルタイムで時刻を表示
  • メモモード: 実際にメモも取れる
  • 隠しボタン: 長押しや特殊な操作で録音開始
  • 自然な動作: 通常のアプリと同じ挙動
  • 録音中の表示: ごく小さなインジケーターのみ
📱 iOS – 電卓モード実装(Swift)
import UIKit

class CalculatorViewController: UIViewController {
    @IBOutlet weak var displayLabel: UILabel!
    
    var currentNumber: String = "0"
    var previousNumber: String = ""
    var operation: String = ""
    var isRecording: Bool = false
    
    // 隠しボタン(ACボタンを5秒長押し)
    @IBAction func secretButtonLongPress(_ gesture: UILongPressGestureRecognizer) {
        if gesture.state == .began {
            // 5秒長押しで録音開始/停止
            toggleRecording()
        }
    }
    
    @IBAction func numberPressed(_ sender: UIButton) {
        let number = sender.titleLabel?.text ?? ""
        
        if currentNumber == "0" {
            currentNumber = number
        } else {
            currentNumber += number
        }
        
        displayLabel.text = currentNumber
    }
    
    @IBAction func operationPressed(_ sender: UIButton) {
        let op = sender.titleLabel?.text ?? ""
        previousNumber = currentNumber
        currentNumber = "0"
        operation = op
    }
    
    @IBAction func equalsPressed(_ sender: UIButton) {
        guard let prev = Double(previousNumber),
              let curr = Double(currentNumber) else { return }
        
        var result: Double = 0
        
        switch operation {
        case "+":
            result = prev + curr
        case "-":
            result = prev - curr
        case "×":
            result = prev * curr
        case "÷":
            result = curr != 0 ? prev / curr : 0
        default:
            break
        }
        
        currentNumber = String(result)
        displayLabel.text = currentNumber
        operation = ""
        previousNumber = ""
    }
    
    func toggleRecording() {
        if isRecording {
            stopRecording()
        } else {
            startRecording()
        }
        isRecording.toggle()
    }
    
    func startRecording() {
        // 小さな録音インジケーターを表示
        showRecordingIndicator()
        
        // 録音開始
        AudioRecorder.shared.startRecording()
        
        // 触覚フィードバック(バイブ)
        let generator = UINotificationFeedbackGenerator()
        generator.notificationOccurred(.success)
    }
    
    func stopRecording() {
        hideRecordingIndicator()
        AudioRecorder.shared.stopRecording()
        
        let generator = UINotificationFeedbackGenerator()
        generator.notificationOccurred(.success)
    }
    
    func showRecordingIndicator() {
        // 右上に小さな赤い点を表示
        let indicator = UIView(frame: CGRect(x: view.frame.width - 30, 
                                             y: 20, 
                                             width: 10, 
                                             height: 10))
        indicator.backgroundColor = .red
        indicator.layer.cornerRadius = 5
        indicator.tag = 999
        indicator.alpha = 0.7
        view.addSubview(indicator)
        
        // 点滅アニメーション
        UIView.animate(withDuration: 1.0, 
                      delay: 0, 
                      options: [.repeat, .autoreverse], 
                      animations: {
            indicator.alpha = 0.3
        })
    }
    
    func hideRecordingIndicator() {
        view.viewWithTag(999)?.removeFromSuperview()
    }
}

💡 自然な偽装: 電卓は実際に計算できるようにする。時計は正確な時刻を表示。メモは本当にメモが取れる。「完璧に本物」であることが重要です。

3
高音質バックグラウンド録音

画面を閉じても録音を継続

  • バックグラウンド録音: アプリを閉じても録音継続
  • 高音質設定: 44.1kHz、AAC形式
  • ファイルサイズ最適化: 長時間録音でも容量節約
  • バッテリー最適化: 省電力モードで長時間動作
  • 自動分割: 1時間ごとにファイル分割
  • メタデータ記録: 録音開始時刻、位置情報も保存
🎙️ 高音質録音の実装(Swift)
import AVFoundation
import CoreLocation

class AudioRecorder: NSObject {
    static let shared = AudioRecorder()
    
    private var audioRecorder: AVAudioRecorder?
    private var audioSession: AVAudioSession?
    private var recordingURL: URL?
    private var locationManager: CLLocationManager?
    
    private override init() {
        super.init()
        setupAudioSession()
        setupLocationManager()
    }
    
    func setupAudioSession() {
        audioSession = AVAudioSession.sharedInstance()
        
        do {
            // バックグラウンド録音を有効化
            try audioSession?.setCategory(.record, mode: .default)
            try audioSession?.setActive(true)
            
            // マイクの使用許可をリクエスト
            audioSession?.requestRecordPermission { granted in
                if !granted {
                    print("マイクのアクセス許可が必要です")
                }
            }
        } catch {
            print("オーディオセッションのセットアップ失敗: \(error)")
        }
    }
    
    func startRecording() {
        // 録音ファイル名を生成(タイムスタンプ付き)
        let fileName = "recording_\(Date().timeIntervalSince1970).m4a"
        let documentsPath = FileManager.default.urls(
            for: .documentDirectory, 
            in: .userDomainMask
        )[0]
        recordingURL = documentsPath.appendingPathComponent(fileName)
        
        // 録音設定(高音質)
        let settings: [String: Any] = [
            AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
            AVSampleRateKey: 44100.0,
            AVNumberOfChannelsKey: 2,
            AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue,
            AVEncoderBitRateKey: 128000
        ]
        
        do {
            audioRecorder = try AVAudioRecorder(
                url: recordingURL!, 
                settings: settings
            )
            audioRecorder?.delegate = self
            audioRecorder?.isMeteringEnabled = true
            audioRecorder?.record()
            
            // メタデータを記録
            saveMetadata()
            
            print("録音開始: \(recordingURL!.path)")
        } catch {
            print("録音開始エラー: \(error)")
        }
    }
    
    func stopRecording() {
        audioRecorder?.stop()
        
        // 録音終了時刻を記録
        updateMetadata()
        
        // 自動的にクラウドにアップロード
        uploadToCloud(url: recordingURL!)
        
        print("録音停止")
    }
    
    func saveMetadata() {
        let metadata: [String: Any] = [
            "start_time": Date(),
            "location": getCurrentLocation(),
            "device_info": getDeviceInfo()
        ]
        
        let metadataURL = recordingURL!
            .deletingPathExtension()
            .appendingPathExtension("json")
        
        do {
            let data = try JSONSerialization.data(
                withJSONObject: metadata, 
                options: .prettyPrinted
            )
            try data.write(to: metadataURL)
        } catch {
            print("メタデータ保存エラー: \(error)")
        }
    }
    
    func getCurrentLocation() -> [String: Double] {
        guard let location = locationManager?.location else {
            return ["lat": 0, "lon": 0]
        }
        
        return [
            "lat": location.coordinate.latitude,
            "lon": location.coordinate.longitude
        ]
    }
    
    func getDeviceInfo() -> [String: String] {
        return [
            "model": UIDevice.current.model,
            "os_version": UIDevice.current.systemVersion,
            "device_id": UIDevice.current.identifierForVendor?.uuidString ?? ""
        ]
    }
    
    // バックグラウンドでも録音継続
    func enableBackgroundRecording() {
        // Info.plistに以下を追加:
        // UIBackgroundModes
        // 
        //   audio
        // 
    }
}

extension AudioRecorder: AVAudioRecorderDelegate {
    func audioRecorderDidFinishRecording(
        _ recorder: AVAudioRecorder, 
        successfully flag: Bool
    ) {
        if flag {
            print("録音完了: \(recorder.url.path)")
        }
    }
}

⚠️ バッテリー消費: 長時間録音はバッテリーを消費します。省電力設定を実装し、ユーザーに充電を促す通知を出すと良いでしょう。

4
ファイル暗号化とセキュリティ

録音データを完全に保護

  • AES-256暗号化: 軍事レベルの暗号化
  • パスワード保護: アプリ起動時にパスワード入力
  • 生体認証: Face ID / Touch ID対応
  • ファイル名の難読化: 内容がわからないファイル名
  • 削除時の完全消去: 復元不可能に削除
  • スクリーンショット防止: 録音一覧画面のキャプチャ禁止
🔐 AES-256暗号化の実装
import CryptoSwift

class FileEncryption {
    static let shared = FileEncryption()
    
    // 暗号化キーの生成(ユーザーのパスワードから)
    func generateKey(from password: String) -> [UInt8] {
        let salt = "StealthRecSalt2024".bytes
        let key = try! PKCS5.PBKDF2(
            password: Array(password.utf8),
            salt: salt,
            iterations: 10000,
            keyLength: 32,
            variant: .sha256
        ).calculate()
        return key
    }
    
    // ファイルを暗号化
    func encryptFile(at url: URL, password: String) throws -> URL {
        // 元のファイルを読み込み
        let data = try Data(contentsOf: url)
        
        // 暗号化キーを生成
        let key = generateKey(from: password)
        
        // IV(初期化ベクトル)をランダム生成
        let iv = AES.randomIV(AES.blockSize)
        
        // AES-256-CBCで暗号化
        let aes = try AES(key: key, blockMode: CBC(iv: iv), padding: .pkcs7)
        let encrypted = try aes.encrypt(data.bytes)
        
        // IVと暗号化データを結合
        var encryptedData = Data(iv)
        encryptedData.append(Data(encrypted))
        
        // 暗号化ファイルを保存
        let encryptedURL = url.deletingPathExtension()
            .appendingPathExtension("enc")
        try encryptedData.write(to: encryptedURL)
        
        // 元のファイルを安全に削除
        try secureDelete(at: url)
        
        return encryptedURL
    }
    
    // ファイルを復号化
    func decryptFile(at url: URL, password: String) throws -> URL {
        // 暗号化ファイルを読み込み
        let encryptedData = try Data(contentsOf: url)
        
        // IVを抽出
        let ivSize = AES.blockSize
        let iv = Array(encryptedData.prefix(ivSize))
        let ciphertext = Array(encryptedData.suffix(from: ivSize))
        
        // 暗号化キーを生成
        let key = generateKey(from: password)
        
        // 復号化
        let aes = try AES(key: key, blockMode: CBC(iv: iv), padding: .pkcs7)
        let decrypted = try aes.decrypt(ciphertext)
        
        // 復号化データを保存
        let decryptedURL = url.deletingPathExtension()
            .appendingPathExtension("m4a")
        try Data(decrypted).write(to: decryptedURL)
        
        return decryptedURL
    }
    
    // ファイルを安全に削除(復元不可能)
    func secureDelete(at url: URL) throws {
        let fileSize = try FileManager.default.attributesOfItem(
            atPath: url.path
        )[.size] as! Int
        
        // ランダムデータで上書き(3回)
        for _ in 0..<3 {
            let randomData = Data(
                (0.. Void) {
        let context = LAContext()
        var error: NSError?
        
        if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, 
                                    error: &error) {
            let reason = "録音ファイルにアクセスするには認証が必要です"
            
            context.evaluatePolicy(
                .deviceOwnerAuthenticationWithBiometrics,
                localizedReason: reason
            ) { success, error in
                DispatchQueue.main.async {
                    completion(success)
                }
            }
        } else {
            completion(false)
        }
    }
}

💡 セキュリティ強化: パスワードを忘れた場合の復旧方法も用意しましょう。秘密の質問やメールでの復旧キー送信などを実装すると安心です。

5
自動クラウドバックアップ

端末を壊されても証拠は残る

  • 自動アップロード: 録音終了後すぐにクラウドへ
  • Firebase Storage: 安全で高速なストレージ
  • バックグラウンドアップロード: アプリを閉じても継続
  • 暗号化して保存: クラウド上でも暗号化状態
  • 複数デバイス同期: 別の端末からもアクセス可能
  • 削除保護: クラウド上のファイルは30日間保持
☁️ Firebase Storageへのアップロード
import FirebaseStorage
import FirebaseAuth

class CloudBackup {
    static let shared = CloudBackup()
    private let storage = Storage.storage()
    
    func uploadRecording(fileURL: URL, completion: @escaping (Bool) -> Void) {
        // ユーザーIDを取得
        guard let userId = Auth.auth().currentUser?.uid else {
            completion(false)
            return
        }
        
        // ファイルを暗号化
        let password = KeychainHelper.getPassword()
        guard let encryptedURL = try? FileEncryption.shared
            .encryptFile(at: fileURL, password: password) else {
            completion(false)
            return
        }
        
        // Storageパスを作成(ユーザーごとに分離)
        let fileName = encryptedURL.lastPathComponent
        let storagePath = "recordings/\(userId)/\(fileName)"
        let storageRef = storage.reference().child(storagePath)
        
        // メタデータを設定
        let metadata = StorageMetadata()
        metadata.contentType = "application/octet-stream"
        metadata.customMetadata = [
            "timestamp": "\(Date().timeIntervalSince1970)",
            "device_id": UIDevice.current.identifierForVendor?.uuidString ?? "",
            "encrypted": "true"
        ]
        
        // バックグラウンドでアップロード
        let uploadTask = storageRef.putFile(
            from: encryptedURL, 
            metadata: metadata
        ) { metadata, error in
            if let error = error {
                print("アップロードエラー: \(error)")
                completion(false)
                return
            }
            
            print("アップロード完了: \(storagePath)")
            
            // ダウンロードURLを取得して保存
            storageRef.downloadURL { url, error in
                if let downloadURL = url {
                    self.saveDownloadURL(
                        fileName: fileName, 
                        url: downloadURL.absoluteString
                    )
                }
            }
            
            completion(true)
        }
        
        // アップロード進捗を監視
        uploadTask.observe(.progress) { snapshot in
            let percentComplete = 100.0 * Double(snapshot.progress!.completedUnitCount) 
                / Double(snapshot.progress!.totalUnitCount)
            print("アップロード進捗: \(percentComplete)%")
        }
    }
    
    func downloadRecording(fileName: String, completion: @escaping (URL?) -> Void) {
        guard let userId = Auth.auth().currentUser?.uid else {
            completion(nil)
            return
        }
        
        let storagePath = "recordings/\(userId)/\(fileName)"
        let storageRef = storage.reference().child(storagePath)
        
        // ローカルの一時ファイルパス
        let localURL = FileManager.default.temporaryDirectory
            .appendingPathComponent(fileName)
        
        // ダウンロード
        let downloadTask = storageRef.write(toFile: localURL) { url, error in
            if let error = error {
                print("ダウンロードエラー: \(error)")
                completion(nil)
                return
            }
            
            // 復号化
            let password = KeychainHelper.getPassword()
            if let decryptedURL = try? FileEncryption.shared
                .decryptFile(at: localURL, password: password) {
                completion(decryptedURL)
            } else {
                completion(nil)
            }
        }
        
        downloadTask.observe(.progress) { snapshot in
            let percentComplete = 100.0 * Double(snapshot.progress!.completedUnitCount) 
                / Double(snapshot.progress!.totalUnitCount)
            print("ダウンロード進捗: \(percentComplete)%")
        }
    }
    
    func listAllRecordings(completion: @escaping ([String]) -> Void) {
        guard let userId = Auth.auth().currentUser?.uid else {
            completion([])
            return
        }
        
        let storageRef = storage.reference().child("recordings/\(userId)")
        
        storageRef.listAll { result, error in
            if let error = error {
                print("リスト取得エラー: \(error)")
                completion([])
                return
            }
            
            let fileNames = result?.items.map { $0.name } ?? []
            completion(fileNames)
        }
    }
    
    func deleteRecording(fileName: String, completion: @escaping (Bool) -> Void) {
        guard let userId = Auth.auth().currentUser?.uid else {
            completion(false)
            return
        }
        
        let storagePath = "recordings/\(userId)/\(fileName)"
        let storageRef = storage.reference().child(storagePath)
        
        storageRef.delete { error in
            if let error = error {
                print("削除エラー: \(error)")
                completion(false)
            } else {
                print("削除完了: \(fileName)")
                completion(true)
            }
        }
    }
    
    private func saveDownloadURL(fileName: String, url: String) {
        // データベースにダウンロードURLを保存
        let db = Firestore.firestore()
        let userId = Auth.auth().currentUser?.uid ?? ""
        
        db.collection("recordings").document(fileName).setData([
            "user_id": userId,
            "download_url": url,
            "created_at": FieldValue.serverTimestamp()
        ])
    }
}

⚠️ ストレージ容量: Firebase Storageの無料枠は5GB。有料プランは従量課金なので、長時間録音が多いユーザーにはコストがかかります。料金プランの設計が重要です。

6
録音ファイル管理画面

録音の再生・整理・共有機能

  • リスト表示: 録音日時、長さ、サイズを表示
  • 再生機能: 倍速再生、巻き戻し・早送り
  • 波形表示: 音声の波形を可視化
  • タグ付け: 録音にメモやタグを追加
  • 共有機能: 弁護士や警察に安全に共有
  • 削除確認: 重要な証拠を誤って削除しない
🎵 録音再生プレイヤーの実装
import AVFoundation

class AudioPlayer: NSObject {
    static let shared = AudioPlayer()
    
    private var audioPlayer: AVAudioPlayer?
    private var timer: Timer?
    
    var isPlaying: Bool {
        return audioPlayer?.isPlaying ?? false
    }
    
    var currentTime: TimeInterval {
        return audioPlayer?.currentTime ?? 0
    }
    
    var duration: TimeInterval {
        return audioPlayer?.duration ?? 0
    }
    
    func play(url: URL, completion: @escaping () -> Void) {
        do {
            audioPlayer = try AVAudioPlayer(contentsOf: url)
            audioPlayer?.delegate = self
            audioPlayer?.prepareToPlay()
            audioPlayer?.play()
            
            // 再生進捗を監視
            timer = Timer.scheduledTimer(
                withTimeInterval: 0.1, 
                repeats: true
            ) { _ in
                NotificationCenter.default.post(
                    name: .audioPlayerProgressUpdate,
                    object: nil,
                    userInfo: [
                        "currentTime": self.currentTime,
                        "duration": self.duration
                    ]
                )
            }
        } catch {
            print("再生エラー: \(error)")
        }
    }
    
    func pause() {
        audioPlayer?.pause()
        timer?.invalidate()
    }
    
    func resume() {
        audioPlayer?.play()
    }
    
    func stop() {
        audioPlayer?.stop()
        timer?.invalidate()
        audioPlayer = nil
    }
    
    func seek(to time: TimeInterval) {
        audioPlayer?.currentTime = time
    }
    
    func setPlaybackRate(_ rate: Float) {
        audioPlayer?.enableRate = true
        audioPlayer?.rate = rate // 0.5 = 半速, 1.0 = 等速, 2.0 = 2倍速
    }
}

extension AudioPlayer: AVAudioPlayerDelegate {
    func audioPlayerDidFinishPlaying(
        _ player: AVAudioPlayer, 
        successfully flag: Bool
    ) {
        timer?.invalidate()
        NotificationCenter.default.post(
            name: .audioPlayerDidFinish,
            object: nil
        )
    }
}

// 録音リスト画面
class RecordingsViewController: UITableViewController {
    var recordings: [Recording] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        loadRecordings()
    }
    
    func loadRecordings() {
        // ローカルとクラウドの録音を取得
        RecordingManager.shared.getAllRecordings { recordings in
            self.recordings = recordings.sorted { 
                $0.createdAt > $1.createdAt 
            }
            self.tableView.reloadData()
        }
    }
    
    override func tableView(
        _ tableView: UITableView,
        numberOfRowsInSection section: Int
    ) -> Int {
        return recordings.count
    }
    
    override func tableView(
        _ tableView: UITableView,
        cellForRowAt indexPath: IndexPath
    ) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(
            withIdentifier: "RecordingCell",
            for: indexPath
        )
        
        let recording = recordings[indexPath.row]
        
        cell.textLabel?.text = recording.title
        cell.detailTextLabel?.text = """
            \(formatDate(recording.createdAt)) | \
            \(formatDuration(recording.duration))
        """
        
        return cell
    }
    
    override func tableView(
        _ tableView: UITableView,
        didSelectRowAt indexPath: IndexPath
    ) {
        let recording = recordings[indexPath.row]
        showPlayerViewController(recording: recording)
    }
    
    func formatDate(_ date: Date) -> String {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy/MM/dd HH:mm"
        return formatter.string(from: date)
    }
    
    func formatDuration(_ duration: TimeInterval) -> String {
        let hours = Int(duration) / 3600
        let minutes = Int(duration) / 60 % 60
        let seconds = Int(duration) % 60
        
        if hours > 0 {
            return String(format: "%d:%02d:%02d", hours, minutes, seconds)
        } else {
            return String(format: "%02d:%02d", minutes, seconds)
        }
    }
}

💡 UX向上: 録音ファイルに自動でタイトルを生成(例:「2024年11月30日 14時の録音」)。ユーザーが後で編集できるようにすると使いやすくなります。

7
証拠性の確保(タイムスタンプ・位置情報)

法的に有効な証拠として使える仕組み

  • タイムスタンプ: 録音開始・終了の正確な時刻
  • 位置情報: GPS座標を記録(任意)
  • デバイス情報: 端末情報、OS版号
  • 改ざん防止: ハッシュ値で完全性を保証
  • チェーンオブカストディ: 誰がいつアクセスしたか記録
  • 証明書発行: 録音の真正性を証明する文書
📜 証拠性確保の実装
import CryptoKit
import CoreLocation

class EvidenceManager {
    static let shared = EvidenceManager()
    
    func createEvidence(for recording: Recording) -> Evidence {
        let fileURL = recording.fileURL
        let fileData = try! Data(contentsOf: fileURL)
        
        // SHA-256ハッシュを計算
        let hash = SHA256.hash(data: fileData)
        let hashString = hash.compactMap { 
            String(format: "%02x", $0) 
        }.joined()
        
        let evidence = Evidence(
            recordingId: recording.id,
            fileName: recording.fileName,
            fileSize: fileData.count,
            sha256Hash: hashString,
            recordingStartTime: recording.startTime,
            recordingEndTime: recording.endTime,
            duration: recording.duration,
            location: recording.location,
            deviceInfo: getDeviceInfo(),
            createdAt: Date()
        )
        
        // 証拠情報をデータベースに保存
        saveEvidence(evidence)
        
        // ブロックチェーンにハッシュを記録(オプション)
        recordToBlockchain(hash: hashString)
        
        return evidence
    }
    
    func generateCertificate(for evidence: Evidence) -> Data {
        let certificate = """
        ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
        録音証明書
        ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
        
        録音ID: \(evidence.recordingId)
        ファイル名: \(evidence.fileName)
        
        【録音情報】
        開始時刻: \(formatDateTime(evidence.recordingStartTime))
        終了時刻: \(formatDateTime(evidence.recordingEndTime))
        録音時間: \(formatDuration(evidence.duration))
        ファイルサイズ: \(formatFileSize(evidence.fileSize))
        
        【位置情報】
        緯度: \(evidence.location.latitude)
        経度: \(evidence.location.longitude)
        
        【デバイス情報】
        機種: \(evidence.deviceInfo.model)
        OS: \(evidence.deviceInfo.osVersion)
        
        【完全性情報】
        SHA-256ハッシュ:
        \(evidence.sha256Hash)
        
        このファイルは上記ハッシュ値により
        改ざんされていないことが証明されます。
        
        発行日時: \(formatDateTime(Date()))
        
        ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
        """
        
        return certificate.data(using: .utf8)!
    }
    
    func verifyIntegrity(recording: Recording) -> Bool {
        // ファイルの現在のハッシュを計算
        let fileURL = recording.fileURL
        guard let fileData = try? Data(contentsOf: fileURL) else {
            return false
        }
        
        let currentHash = SHA256.hash(data: fileData)
        let currentHashString = currentHash.compactMap { 
            String(format: "%02x", $0) 
        }.joined()
        
        // 保存されているハッシュと比較
        guard let evidence = getEvidence(for: recording.id) else {
            return false
        }
        
        return currentHashString == evidence.sha256Hash
    }
    
    func recordAccess(recordingId: String, action: String) {
        let accessLog = AccessLog(
            recordingId: recordingId,
            userId: getCurrentUserId(),
            action: action, // "view", "play", "share", "delete"
            timestamp: Date(),
            ipAddress: getIPAddress(),
            deviceId: getDeviceId()
        )
        
        saveAccessLog(accessLog)
    }
    
    private func recordToBlockchain(hash: String) {
        // ブロックチェーン(Ethereum等)にハッシュを記録
        // タイムスタンプ証明として使用可能
        // 実装例: Infura API経由でEthereumに記録
    }
}

struct Evidence: Codable {
    let recordingId: String
    let fileName: String
    let fileSize: Int
    let sha256Hash: String
    let recordingStartTime: Date
    let recordingEndTime: Date
    let duration: TimeInterval
    let location: LocationInfo
    let deviceInfo: DeviceInfo
    let createdAt: Date
}

struct LocationInfo: Codable {
    let latitude: Double
    let longitude: Double
    let accuracy: Double
}

struct DeviceInfo: Codable {
    let model: String
    let osVersion: String
    let deviceId: String
}

⚠️ 法的助言: このアプリはあくまで証拠収集の補助ツールです。録音の法的有効性は弁護士に相談することをユーザーに推奨してください。

8
App Store審査対策とデプロイ

審査を通過するための重要ポイント

  • 利用規約の明記: 「自己防衛・証拠保全用」と明示
  • 違法使用の禁止: 盗聴は違法であることを警告
  • プライバシーポリシー: データの取り扱いを詳細に記載
  • マイク使用理由: 「録音のため」と正直に記載
  • 位置情報理由: 「証拠保全のため」と説明
  • 審査時の説明: レビューノートで用途を詳しく説明
📝 Info.plist設定(重要)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN">
<plist version="1.0">
<dict>
    <!-- マイク使用許可 -->
    <key>NSMicrophoneUsageDescription</key>
    <string>このアプリは証拠保全のための録音機能を提供します。録音はユーザーの明示的な操作によってのみ開始されます。</string>
    
    <!-- 位置情報使用許可 -->
    <key>NSLocationWhenInUseUsageDescription</key>
    <string>録音時の位置情報を記録することで、証拠としての信頼性を高めます。位置情報の記録は任意です。</string>
    
    <!-- バックグラウンド録音 -->
    <key>UIBackgroundModes</key>
    <array>
        <string>audio</string>
    </array>
    
    <!-- App Transport Security -->
    <key>NSAppTransportSecurity</key>
    <dict>
        <key>NSAllowsArbitraryLoads</key>
        <false/>
    </dict>
</dict>
</plist>
📋 App Store審査ノート(例文)
【アプリの目的】
このアプリは、パワハラ・セクハラ・DV被害者が
自己防衛のために証拠を記録するためのツールです。

【主な機能】
1. 高音質録音
2. ファイル暗号化(AES-256)
3. 自動クラウドバックアップ
4. タイムスタンプ・位置情報記録
5. 証明書発行機能

【法的コンプライアンス】
- 利用規約で「自分が会話の当事者である場合のみ使用可能」と明記
- 違法な盗聴には使用しないよう警告を表示
- プライバシーポリシーで全データの取り扱いを説明

【偽装機能について】
電卓・時計モードは、加害者に録音を気づかれずに
被害者が安全に証拠を残すための機能です。
第三者の会話を盗聴する目的ではありません。

【テストアカウント】
Email: test@stealthrec.com
Password: TestPass123!

録音を開始するには、電卓画面で「AC」ボタンを5秒長押ししてください。
🚀 デプロイチェックリスト
✅ 利用規約 ✅ プライバシーポリシー ✅ サポートURL ✅ スクリーンショット ✅ アプリ説明文 ✅ レビューノート

💡 審査のコツ: 「護身用ツール」であることを前面に出し、社会的意義を強調しましょう。弁護士監修の利用規約があると信頼性が高まります。

💰 開発・運用コスト見積もり
初期開発費用(MVP) 100〜150万円
サーバー費用(Firebase) 月5,000〜20,000円
ストレージ費用(S3/Firebase) 月3,000〜15,000円
Apple Developer Program 年12,980円
Google Play Developer 初回3,300円
SSL証明書・ドメイン 年3,000〜5,000円
月額運用費(目安) 1〜5万円
本格版開発費用 200〜350万円
💡 収益シミュレーション:
月額680円 × 3,000人 = 月間売上204万円
月額680円 × 10,000人 = 月間売上680万円
→ 初期投資を3〜6ヶ月で回収可能
✅ 成功させるための重要ポイント
1. 完璧なステルス性を実現
• 偽装画面は本物として完全に機能させる
• 録音中のインジケーターは極小(1ピクセルの赤点など)
• 通知音・バイブは一切なし
• バッテリー消費も通常アプリと変わらない程度に

2. 社会的意義を前面に
• 「被害者を守るツール」として位置づける
• パワハラ・セクハラ被害の実態を説明
• 弁護士や支援団体と提携してPR
• メディアに取り上げてもらう(社会問題として)

3. 法的リスクの回避
• 利用規約で違法使用を明確に禁止
• 「自分が会話の当事者である場合のみ使用可能」と明記
• 弁護士監修の規約を用意
• アプリ起動時に必ず警告画面を表示

4. 証拠としての信頼性
• タイムスタンプ、位置情報、ハッシュ値で改ざん防止
• 証明書発行機能で法廷でも使える
• クラウドバックアップで端末破壊にも対応
• 弁護士への共有機能を実装

5. ターゲットを明確に
• パワハラ被害者、DV被害者、詐欺被害防止
• 女性向けマーケティングが効果的
• SNSで体験談(匿名)をシェア
• 「いざという時の保険」として訴求
❌ よくある失敗パターン
失敗1: App Store審査で却下
→ 解決策: 「護身用ツール」であることを明確に説明。レビューノートで詳細に用途を記載。

失敗2: 偽装がバレバレ
→ 解決策: 電卓は実際に計算できる、時計は正確な時刻を表示。本物として完全に機能させる。

失敗3: バッテリーがすぐ切れる
→ 解決策: 録音品質を調整可能に。省電力モードを実装。バッテリー警告を出す。

失敗4: 音質が悪くて使えない
→ 解決策: 44.1kHz AAC形式で高音質録音。ノイズキャンセリング機能を追加。

失敗5: 法的トラブル
→ 解決策: 利用規約を弁護士に監修してもらう。違法使用は即アカウント停止。

WordPress自動投稿プラグイン | X(Twitter)へ自動シェア – 無料

AIペット飼育アプリの作り方|リアル生活連動型の完全実装ガイド