ð StealthRec
å®ç§ãªã¹ãã«ã¹æ§ – 誰ã«ãæ°ã¥ãããªãé²é³ã¢ããª
â ïž éèŠ: ãã®ã¢ããªã¯èªå·±é²è¡ã»èšŒæ ä¿å šãç®çãšããŠããŸããéæ³ãªçèŽã«ã¯äœ¿çšããªãã§ãã ãããèªåãäŒè©±ã®åœäºè ã§ããå Žåã®é²é³ã¯åæ³ã§ãã
ã¹ãã«ã¹é²é³ã¢ããªã®æ§ç¯æé
å®ç§ãªåœè£ à é«é³è³ªé²é³ à èªåã¯ã©ãŠãããã¯ã¢ãã
ãã€ãã£ãã¢ããªã§æé«ã®ã¹ãã«ã¹æ§ãå®çŸ
- éçºèšèª: SwiftïŒiOSïŒãKotlinïŒAndroidïŒ
- é³å£°é²é³: AVAudioRecorderïŒiOSïŒãMediaRecorderïŒAndroidïŒ
- ããã¯ã°ã©ãŠã³ãåäœ: Background Modesèšå®
- ã¹ãã¬ãŒãž: Firebase StorageãAWS S3
- ããŒã¿ããŒã¹: RealmãCore DataïŒããŒã«ã«ïŒ
- æå·å: AES-256ã§é²é³ãã¡ã€ã«ãæå·å
# 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
å®ç§ã«æ¬ç©ã«èŠããåœè£ ç»é¢ãäœæ
- é»åã¢ãŒã: å®éã«èšç®ã§ããæ¬ç©ã®é»åãšããŠæ©èœ
- æèšã¢ãŒã: ãªã¢ã«ã¿ã€ã ã§æå»ã衚瀺
- ã¡ã¢ã¢ãŒã: å®éã«ã¡ã¢ãåãã
- é ããã¿ã³: é·æŒããç¹æ®ãªæäœã§é²é³éå§
- èªç¶ãªåäœ: éåžžã®ã¢ããªãšåãæå
- é²é³äžã®è¡šç€º: ããå°ããªã€ã³ãžã±ãŒã¿ãŒã®ã¿
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()
}
}
ð¡ èªç¶ãªåœè£ : é»åã¯å®éã«èšç®ã§ããããã«ãããæèšã¯æ£ç¢ºãªæå»ã衚瀺ãã¡ã¢ã¯æ¬åœã«ã¡ã¢ãåããããå®ç§ã«æ¬ç©ãã§ããããšãéèŠã§ãã
ç»é¢ãéããŠãé²é³ãç¶ç¶
- ããã¯ã°ã©ãŠã³ãé²é³: ã¢ããªãéããŠãé²é³ç¶ç¶
- é«é³è³ªèšå®: 44.1kHzãAAC圢åŒ
- ãã¡ã€ã«ãµã€ãºæé©å: é·æéé²é³ã§ã容éç¯çŽ
- ããããªãŒæé©å: çé»åã¢ãŒãã§é·æéåäœ
- èªååå²: 1æéããšã«ãã¡ã€ã«åå²
- ã¡ã¿ããŒã¿èšé²: é²é³éå§æå»ãäœçœ®æ å ±ãä¿å
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)")
}
}
}
â ïž ããããªãŒæ¶è²»: é·æéé²é³ã¯ããããªãŒãæ¶è²»ããŸããçé»åèšå®ãå®è£ ãããŠãŒã¶ãŒã«å é»ãä¿ãéç¥ãåºããšè¯ãã§ãããã
é²é³ããŒã¿ãå®å šã«ä¿è·
- AES-256æå·å: è»äºã¬ãã«ã®æå·å
- ãã¹ã¯ãŒãä¿è·: ã¢ããªèµ·åæã«ãã¹ã¯ãŒãå ¥å
- çäœèªèšŒ: Face ID / Touch ID察å¿
- ãã¡ã€ã«åã®é£èªå: å 容ãããããªããã¡ã€ã«å
- å逿ã®å®å šæ¶å»: 埩å äžå¯èœã«åé€
- ã¹ã¯ãªãŒã³ã·ã§ãã鲿¢: é²é³äžèЧç»é¢ã®ãã£ããã£çŠæ¢
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)
}
}
}
ð¡ ã»ãã¥ãªãã£åŒ·å: ãã¹ã¯ãŒããå¿ããå Žåã®åŸ©æ§æ¹æ³ãçšæããŸããããç§å¯ã®è³ªåãã¡ãŒã«ã§ã®åŸ©æ§ããŒéä¿¡ãªã©ãå®è£ ãããšå®å¿ã§ãã
端æ«ãå£ãããŠã蚌æ ã¯æ®ã
- èªåã¢ããããŒã: é²é³çµäºåŸããã«ã¯ã©ãŠããž
- Firebase Storage: å®å šã§é«éãªã¹ãã¬ãŒãž
- ããã¯ã°ã©ãŠã³ãã¢ããããŒã: ã¢ããªãéããŠãç¶ç¶
- æå·åããŠä¿å: ã¯ã©ãŠãäžã§ãæå·åç¶æ
- è€æ°ããã€ã¹åæ: å¥ã®ç«¯æ«ãããã¢ã¯ã»ã¹å¯èœ
- åé€ä¿è·: ã¯ã©ãŠãäžã®ãã¡ã€ã«ã¯30æ¥éä¿æ
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ãææãã©ã³ã¯åŸé課éãªã®ã§ãé·æéé²é³ãå€ããŠãŒã¶ãŒã«ã¯ã³ã¹ããããããŸããæéãã©ã³ã®èšèšãéèŠã§ãã
é²é³ã®åçã»æŽçã»å ±ææ©èœ
- ãªã¹ã衚瀺: é²é³æ¥æãé·ãããµã€ãºã衚瀺
- åçæ©èœ: åéåçãå·»ãæ»ãã»æ©éã
- 波圢衚瀺: é³å£°ã®æ³¢åœ¢ãå¯èŠå
- ã¿ã°ä»ã: é²é³ã«ã¡ã¢ãã¿ã°ã远å
- å ±ææ©èœ: åŒè·å£«ãèŠå¯ã«å®å šã«å ±æ
- åé€ç¢ºèª: éèŠãªèšŒæ ã誀ã£ãŠåé€ããªã
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æã®é²é³ãïŒããŠãŒã¶ãŒãåŸã§ç·šéã§ããããã«ãããšäœ¿ãããããªããŸãã
æ³çã«æå¹ãªèšŒæ ãšããŠäœ¿ããä»çµã¿
- ã¿ã€ã ã¹ã¿ã³ã: é²é³éå§ã»çµäºã®æ£ç¢ºãªæå»
- äœçœ®æ å ±: 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
}
â ïž æ³çå©èš: ãã®ã¢ããªã¯ãããŸã§èšŒæ åéã®è£å©ããŒã«ã§ããé²é³ã®æ³çæå¹æ§ã¯åŒè·å£«ã«çžè«ããããšããŠãŒã¶ãŒã«æšå¥šããŠãã ããã
審æ»ãééããããã®éèŠãã€ã³ã
- å©çšèŠçŽã®æèš: ãèªå·±é²è¡ã»èšŒæ ä¿å šçšããšæç€º
- éæ³äœ¿çšã®çŠæ¢: çèŽã¯éæ³ã§ããããšãèŠå
- ãã©ã€ãã·ãŒããªã·ãŒ: ããŒã¿ã®åãæ±ãã詳现ã«èšèŒ
- ãã€ã¯äœ¿çšçç±: ãé²é³ã®ããããšæ£çŽã«èšèŒ
- äœçœ®æ å ±çç±: ã蚌æ ä¿å šã®ããããšèª¬æ
- å¯©æ»æã®èª¬æ: ã¬ãã¥ãŒããŒãã§çšéã詳ãã説æ
<?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>
ãã¢ããªã®ç®çã ãã®ã¢ããªã¯ããã¯ãã©ã»ã»ã¯ãã©ã»DV被害è ã èªå·±é²è¡ã®ããã«èšŒæ ãèšé²ããããã®ããŒã«ã§ãã ãäž»ãªæ©èœã 1. é«é³è³ªé²é³ 2. ãã¡ã€ã«æå·åïŒAES-256ïŒ 3. èªåã¯ã©ãŠãããã¯ã¢ãã 4. ã¿ã€ã ã¹ã¿ã³ãã»äœçœ®æ å ±èšé² 5. èšŒææžçºè¡æ©èœ ãæ³çã³ã³ãã©ã€ã¢ã³ã¹ã - å©çšèŠçŽã§ãèªåãäŒè©±ã®åœäºè ã§ããå Žåã®ã¿äœ¿çšå¯èœããšæèš - éæ³ãªçèŽã«ã¯äœ¿çšããªãããèŠåã衚瀺 - ãã©ã€ãã·ãŒããªã·ãŒã§å šããŒã¿ã®åãæ±ãã説æ ãåœè£ æ©èœã«ã€ããŠã é»åã»æèšã¢ãŒãã¯ãå 害è ã«é²é³ãæ°ã¥ããã㫠被害è ãå®å šã«èšŒæ ãæ®ãããã®æ©èœã§ãã 第äžè ã®äŒè©±ãçèŽããç®çã§ã¯ãããŸããã ããã¹ãã¢ã«ãŠã³ãã Email: test@stealthrec.com Password: TestPass123! é²é³ãéå§ããã«ã¯ãé»åç»é¢ã§ãACããã¿ã³ã5ç§é·æŒãããŠãã ããã
ð¡ 審æ»ã®ã³ã: ãè·èº«çšããŒã«ãã§ããããšãåé¢ã«åºãã瀟äŒçæçŸ©ã匷調ããŸããããåŒè·å£«ç£ä¿®ã®å©çšèŠçŽããããšä¿¡é Œæ§ãé«ãŸããŸãã
æé¡680å à 3,000人 = æé売äž204äžå
æé¡680å à 10,000人 = æé売äž680äžå
â åææè³ã3ã6ã¶æã§ååå¯èœ
⢠åœè£ ç»é¢ã¯æ¬ç©ãšããŠå®å šã«æ©èœããã
⢠é²é³äžã®ã€ã³ãžã±ãŒã¿ãŒã¯æ¥µå°ïŒ1ãã¯ã»ã«ã®èµ€ç¹ãªã©ïŒ
⢠éç¥é³ã»ãã€ãã¯äžåãªã
⢠ããããªãŒæ¶è²»ãéåžžã¢ããªãšå€ãããªãçšåºŠã«
2. 瀟äŒçæçŸ©ãåé¢ã«
⢠ã被害è ãå®ãããŒã«ããšããŠäœçœ®ã¥ãã
⢠ãã¯ãã©ã»ã»ã¯ãã©è¢«å®³ã®å®æ ã説æ
⢠åŒè·å£«ãæ¯æŽå£äœãšææºããŠPR
⢠ã¡ãã£ã¢ã«åãäžããŠãããïŒç€ŸäŒåé¡ãšããŠïŒ
3. æ³çãªã¹ã¯ã®åé¿
⢠å©çšèŠçŽã§éæ³äœ¿çšãæç¢ºã«çŠæ¢
⢠ãèªåãäŒè©±ã®åœäºè ã§ããå Žåã®ã¿äœ¿çšå¯èœããšæèš
⢠åŒè·å£«ç£ä¿®ã®èŠçŽãçšæ
⢠ã¢ããªèµ·åæã«å¿ ãèŠåç»é¢ã衚瀺
4. 蚌æ ãšããŠã®ä¿¡é Œæ§
⢠ã¿ã€ã ã¹ã¿ã³ããäœçœ®æ å ±ãããã·ã¥å€ã§æ¹ãã鲿¢
â¢ èšŒææžçºè¡æ©èœã§æ³å»·ã§ã䜿ãã
⢠ã¯ã©ãŠãããã¯ã¢ããã§ç«¯æ«ç Žå£ã«ã察å¿
⢠åŒè·å£«ãžã®å ±ææ©èœãå®è£
5. ã¿ãŒã²ãããæç¢ºã«
⢠ãã¯ãã©è¢«å®³è ãDV被害è ãè©æ¬ºè¢«å®³é²æ¢
⢠女æ§åãããŒã±ãã£ã³ã°ã广ç
⢠SNSã§äœéšè«ïŒå¿åïŒãã·ã§ã¢
⢠ããããšããæã®ä¿éºããšããŠèšŽæ±
â 解決ç: ãè·èº«çšããŒã«ãã§ããããšãæç¢ºã«èª¬æãã¬ãã¥ãŒããŒãã§è©³çްã«çšéãèšèŒã
倱æ2: åœè£ ããã¬ãã¬
â 解決ç: é»åã¯å®éã«èšç®ã§ãããæèšã¯æ£ç¢ºãªæå»ãè¡šç€ºãæ¬ç©ãšããŠå®å šã«æ©èœãããã
倱æ3: ããããªãŒãããåãã
â 解決ç: é²é³å質ã調æŽå¯èœã«ãçé»åã¢ãŒããå®è£ ãããããªãŒèŠåãåºãã
倱æ4: é³è³ªãæªããŠäœ¿ããªã
â 解決ç: 44.1kHz AAC圢åŒã§é«é³è³ªé²é³ããã€ãºãã£ã³ã»ãªã³ã°æ©èœã远å ã
倱æ5: æ³çãã©ãã«
â 解決ç: å©çšèŠçŽãåŒè·å£«ã«ç£ä¿®ããŠããããéæ³äœ¿çšã¯å³ã¢ã«ãŠã³ã忢ã
