Androidアプリからのファイルパーミッションの制御

アプリ間のファイルアクセスの許可/禁止には unix のファイルパーミッションの仕組みが使われています。

Androidではアプリごとにuidとgidが割り当てられていて、パーミッションのうち、otherのread,write,execute ビットを変更することで他アプリからのアクセスを許可/禁止することができます。

パーミッションの設定はネイティブコードならchmod(2)を使うんですが、Javaからだとそのまま呼び出せるようにはなっていません。

そこで今回はアプリからファイルパーミッションを制御するにはどのようなAPIを使えばよいか確認してみます。

Javaの標準APIでの制御

Javaの標準では、他ユーザからのアクセスを制御したい場合は java.io.File のメソッドを使います。

  • File#setReadable (boolean readable, boolean ownerOnly)
  • File#setWritable (boolean writable, boolean ownerOnly)
  • File#setExecutable (boolean executable, boolean ownerOnly)

ただしAndroidではこれらのメソッドが提供されるのは API Level 9 (OS 2.3)以降です。
Android Developers の Device Dashboard の統計を見ると、 2012年3月5日現在 でも 2.1と2.2が合わせて32%近くあります。これらの古い端末を無視してしまうのはまだ難しいです。

AndroidAPIでの制御

AndroidAPIでは、以下のメソッドを使うことで他アプリからのアクセスを制限できます。

  • Context#openFileOutput(String name, int mode)
  • Context#openOrCreateDatabase(String name, int mode, CursorFactory factory)
  • Context#getDir(String name, int mode)
ファイルパス指定の制限

これらのメソッドはファイルパスの指定に制限があります。

  • openFileOutput と getDir は パス指定にパス区切りを含めることができません。IllegalArgumentException が発生します。
  • openOrCreateDatabase に相対パスを指定した場合、パス指定にパス区切りを含めることができません。IllegalArgumentException が発生します。
  • openOrCreateDatabase に絶対パスを指定した場合、それは解釈されます。ただしディレクトリを作ってくれるのはパス階層の最後の1段だけです。
ディレクトリのパーミッション

なのでこれらのAPIで作成したフォルダは、他アプリからもchdirが可能となります。

getDir() の制限
  • getDir() で取得したディレクトリの下に任意にファイル階層を作成することはできますが、階層中のファイルに個別にアクセス制御を設定するAPIは提供されていません。つまりgetDir() で作成したフォルダの単位でしかパーミッションを制御できません。
  • getDirがパーミッションを設定するのは「ディレクトリを作成した時」だけです。つまりアプリをアップデートした際、既存のディレクトリのモードは再設定されません。 ちなみに openFileOutput と openOrCreateDatabase は毎回パーミッションを設定するようになってます。 getDir() だけ穴が残ってる感じです。

非公開API android.os.FileUtils#setPermissions

前項で説明した3つのメソッドは、ContextImpl#setFilePermissionsFromMode 経由で非公開クラス android.os.FileUtils の ネイティブメソッド setPermissions を呼び出し、このメソッドがchown(2)とchmod(2)を呼び出しています。ただし setFilePermissionsFromMode は uid,gidに-1を設定していて、その場合は FileUtils#setPermissions は chown(2)を呼び出しません。

試しにリフレクションで FileUtils#setPermissions を呼び出してみましょう。
手元の端末で試してみたら1.6, 2.3.6, 2.3.7, 3.1, 4.0.2 で動いてたので実用上問題はないと思いますが、非公開APIなので取り扱いには注意が必要です。

try{
	context.openFileOutput("test_file",Context.MODE_PRIVATE).close();
	File file = context.getFileStreamPath("test_file");
	String path = file.getPath();
	//
	Method setPermissions= Class.forName("android.os.FileUtils").getMethod("setPermissions",String.class ,int.class ,int.class ,int.class);
	// 
	int rv = ((Integer)(setPermissions.invoke(null,path,0600,-1,-1))).intValue();
	Log.d(TAG,"FileUtils.setPermissions rv="+rv); // returns 0 or errno 
}catch(Throwable ex){
	ex.printStackTrace();
}

おまけ: Context APIが作成するファイルの位置


蛇足ですが、実際にファイルやディレクトリが作成される位置をメモしておきます。
なお、確認にはrooted端末を使用しました。

次のようなコードを実行すると

context.openFileOutput("hoge",Context.MODE_PRIVATE).close();
context.getDir("dir1",Context.MODE_PRIVATE);
context.openOrCreateDatabase("db1",Context.MODE_PRIVATE,null).close();

/data/data/${package_name} の下に 次のようなファイル階層が作成されます。

# pwd
/data/data/jp.juggler.test_app
# find .
.
./databases
./databases/db1
./app_dir1
./files
./files/hoge
./lib

ファイルパスとAPIとの対応はこんな感じです。

API_Level メソッド名 内容 関連API
(private) Context#getDataDirFile LoadedApk#getDataDirFile() 下記すべて
(private) Context#getDatabasesDir getDataDirFile() + "/databases"
(ただし特定の条件で /data/system に変更されます)
databaseList,
getDatabasePath,
deleteDatabase,
openOrCreateDatabase
1 Context#getFilesDir getDataDirFile() + "/files" openFileInput,
openFileOutput,
deleteFile,
getFileStreamPath,
fileList
1 Context#getDir(name) getDataDirFile() + "/app_"+name なし